Add error suffixIcon.

This commit is contained in:
Elias Bonnici 2023-11-24 14:37:37 +01:00
parent 63bb18b2be
commit fa92927f13
No known key found for this signature in database
GPG Key ID: 5EAC28EA3F980CCF
16 changed files with 548 additions and 418 deletions

View File

@ -173,17 +173,29 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
errorText: _pinIsWrong ? _getErrorText() : null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.pin_outlined),
suffixIcon: IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
color: IconTheme.of(context).color,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin,
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
color: !_pinIsWrong
? IconTheme.of(context).color
: null),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin,
),
if (_pinIsWrong) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
onChanged: (value) {

View File

@ -84,6 +84,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
errorText: _currentIsWrong ? _currentPinError : null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.pin_outlined),
suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null,
),
onChanged: (value) {
setState(() {
@ -107,6 +108,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
errorText: _newIsWrong ? _newPinError : null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.pin_outlined),
suffixIcon: _newIsWrong ? const Icon(Icons.error) : null,
),
onChanged: (value) {
setState(() {

View File

@ -19,10 +19,10 @@ import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:yubico_authenticator/core/models.dart';
import '../../android/oath/state.dart';
import '../../app/logging.dart';
@ -49,9 +49,6 @@ import 'utils.dart';
final _log = Logger('oath.view.add_account_page');
final _secretFormatterPattern =
RegExp('[abcdefghijklmnopqrstuvwxyz234567 ]', caseSensitive: false);
class OathAddAccountPage extends ConsumerStatefulWidget {
final DevicePath? devicePath;
final OathState? state;
@ -83,7 +80,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
HashAlgorithm _hashAlgorithm = defaultHashAlgorithm;
int _digits = defaultDigits;
int _counter = defaultCounter;
bool _validateSecretLength = false;
bool _validateSecret = false;
bool _dataLoaded = false;
bool _isObscure = true;
List<int> _periodValues = [20, 30, 45, 60];
@ -235,6 +232,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
final secret = _secretController.text.replaceAll(' ', '');
final secretLengthValid = secret.length * 5 % 8 < 5;
final secretFormatValid = Format.base32.isValid(secret);
// is this credentials name/issuer pair different from all other?
final isUnique = _credentials
@ -271,7 +269,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
}
void submit() async {
if (secretLengthValid) {
if (secretLengthValid && secretFormatValid) {
final cred = CredentialData(
issuer: issuerText.isEmpty ? null : issuerText,
name: nameText,
@ -304,7 +302,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
}
} else {
setState(() {
_validateSecretLength = true;
_validateSecret = true;
});
}
}
@ -368,14 +366,18 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_issuer_optional,
helperText: '',
// Prevents dialog resizing when disabled
prefixIcon: const Icon(Icons.business_outlined),
helperText:
'', // Prevents dialog resizing when disabled
errorText: (byteLength(issuerText) > issuerMaxLength)
? '' // needs empty string to render as error
: issuerNoColon
? null
: l10n.l_invalid_character_issuer,
prefixIcon: const Icon(Icons.business_outlined),
suffixIcon: (!issuerNoColon ||
byteLength(issuerText) > issuerMaxLength)
? const Icon(Icons.error)
: null,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
@ -395,7 +397,6 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
inputFormatters: [limitBytesLength(nameRemaining)],
decoration: InputDecoration(
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.person_outline),
labelText: l10n.s_account_name,
helperText: '',
// Prevents dialog resizing when disabled
@ -404,6 +405,11 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
: isUnique
? null
: l10n.l_name_already_exists,
prefixIcon: const Icon(Icons.person_outline),
suffixIcon:
(!isUnique || byteLength(nameText) > nameMaxLength)
? const Icon(Icons.error)
: null,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
@ -423,38 +429,50 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
// would hint to use saved passwords for this field
autofillHints:
isAndroid ? [] : const [AutofillHints.password],
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
_secretFormatterPattern)
],
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(
_isObscure
? Icons.visibility
: Icons.visibility_off,
color: IconTheme.of(context).color,
border: const OutlineInputBorder(),
labelText: l10n.s_secret_key,
errorText: _validateSecret && !secretLengthValid
? l10n.s_invalid_length
: _validateSecret && !secretFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.base32.allowedCharacters)
: null,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(
_isObscure
? Icons.visibility
: Icons.visibility_off,
color: !_validateSecret
? IconTheme.of(context).color
: null),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure
? l10n.s_show_secret_key
: l10n.s_hide_secret_key,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure
? l10n.s_show_secret_key
: l10n.s_hide_secret_key,
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined),
labelText: l10n.s_secret_key,
errorText: _validateSecretLength && !secretLengthValid
? l10n.s_invalid_length
: null),
if (_validateSecret) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
readOnly: _dataLoaded,
textInputAction: TextInputAction.done,
onChanged: (value) {
setState(() {
_validateSecretLength = false;
_validateSecret = false;
});
},
onSubmitted: (_) {

View File

@ -89,11 +89,13 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
autofillHints: const [AutofillHints.password],
key: keys.currentPasswordField,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_current_password,
prefixIcon: const Icon(Icons.password_outlined),
errorText: _currentIsWrong ? l10n.s_wrong_password : null,
errorMaxLines: 3),
border: const OutlineInputBorder(),
labelText: l10n.s_current_password,
errorText: _currentIsWrong ? l10n.s_wrong_password : null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.password_outlined),
suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {

View File

@ -208,6 +208,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
? l10n.l_name_already_exists
: null,
prefixIcon: const Icon(Icons.people_alt_outlined),
suffixIcon:
!nameNotEmpty || !isUnique ? const Icon(Icons.error) : null,
),
textInputAction: TextInputAction.done,
onChanged: (value) {

View File

@ -85,19 +85,33 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
helperText: '', // Prevents resizing when errorText shown
prefixIcon: const Icon(Icons.password_outlined),
suffixIcon: IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
color: IconTheme.of(context).color,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure
? l10n.s_show_password
: l10n.s_hide_password,
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(
_isObscure
? Icons.visibility
: Icons.visibility_off,
color: !_passwordIsWrong
? IconTheme.of(context).color
: null),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure
? l10n.s_show_password
: l10n.s_hide_password,
),
if (_passwordIsWrong) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
onChanged: (_) => setState(() {

View File

@ -48,8 +48,7 @@ class ConfigureChalrespDialog extends ConsumerStatefulWidget {
class _ConfigureChalrespDialogState
extends ConsumerState<ConfigureChalrespDialog> {
final _secretController = TextEditingController();
bool _validateSecretLength = false;
bool _validateSecretFormat = false;
bool _validateSecret = false;
bool _requireTouch = false;
final int secretMaxLength = 40;
@ -74,17 +73,11 @@ class _ConfigureChalrespDialogState
actions: [
TextButton(
key: keys.saveButton,
onPressed: !_validateSecretLength
onPressed: !_validateSecret
? () async {
if (!secretLengthValid) {
if (!secretLengthValid || !secretFormatValid) {
setState(() {
_validateSecretLength = true;
});
return;
}
if (!secretFormatValid) {
setState(() {
_validateSecretFormat = true;
_validateSecret = true;
});
return;
}
@ -136,48 +129,49 @@ class _ConfigureChalrespDialogState
autofillHints: isAndroid ? [] : const [AutofillHints.password],
maxLength: secretMaxLength,
decoration: InputDecoration(
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
border: const OutlineInputBorder(),
labelText: l10n.s_secret_key,
errorText: _validateSecret && !secretLengthValid
? l10n.s_invalid_length
: _validateSecret && !secretFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
setState(() {
final random = Random.secure();
final key = List.generate(
20,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
final random = Random.secure();
final key = List.generate(
20,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_secretController.text = key;
});
_secretController.text = key;
});
},
),
if (_validateSecretLength || _validateSecretFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined),
labelText: l10n.s_secret_key,
errorText: _validateSecretLength && !secretLengthValid
? l10n.s_invalid_length
: _validateSecretFormat && !secretFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null),
});
},
tooltip: l10n.s_generate_random,
),
if (_validateSecret) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_validateSecretLength = false;
_validateSecretFormat = false;
_validateSecret = false;
});
},
),

View File

@ -47,8 +47,7 @@ class ConfigureHotpDialog extends ConsumerStatefulWidget {
class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
final _secretController = TextEditingController();
bool _validateSecretLength = false;
bool _validateSecretFormat = false;
bool _validateSecret = false;
int _digits = defaultDigits;
final List<int> _digitsValues = [6, 8];
bool _appendEnter = true;
@ -73,17 +72,11 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
actions: [
TextButton(
key: keys.saveButton,
onPressed: !_validateSecretLength
onPressed: !_validateSecret
? () async {
if (!secretLengthValid) {
if (!secretLengthValid || !secretFormatValid) {
setState(() {
_validateSecretLength = true;
});
return;
}
if (!secretFormatValid) {
setState(() {
_validateSecretFormat = true;
_validateSecret = true;
});
return;
}
@ -133,47 +126,47 @@ class _ConfigureHotpDialogState extends ConsumerState<ConfigureHotpDialog> {
obscureText: _isObscure,
autofillHints: isAndroid ? [] : const [AutofillHints.password],
decoration: InputDecoration(
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(
_isObscure
? Icons.visibility
: Icons.visibility_off,
color: !(_validateSecretLength ||
_validateSecretFormat)
? IconTheme.of(context).color
: null),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
),
if (_validateSecretLength || _validateSecretFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined),
labelText: l10n.s_secret_key,
helperText: '', // Prevents resizing when errorText shown
errorText: _validateSecretLength && !secretLengthValid
? l10n.s_invalid_length
: _validateSecretFormat && !secretFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.base32.allowedCharacters)
: null),
border: const OutlineInputBorder(),
labelText: l10n.s_secret_key,
helperText: '', // Prevents resizing when errorText shown
errorText: _validateSecret && !secretLengthValid
? l10n.s_invalid_length
: _validateSecret && !secretFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.base32.allowedCharacters)
: null,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
color: !_validateSecret
? IconTheme.of(context).color
: null),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure
? l10n.s_show_secret_key
: l10n.s_hide_secret_key,
),
if (_validateSecret) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_validateSecretLength = false;
_validateSecretFormat = false;
_validateSecret = false;
});
},
),

View File

@ -149,44 +149,40 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
autofillHints: isAndroid ? [] : const [AutofillHints.password],
maxLength: passwordMaxLength,
decoration: InputDecoration(
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_generate_passowrd,
icon: const Icon(Icons.refresh),
onPressed: () async {
final password = await ref
.read(
otpStateProvider(widget.devicePath).notifier)
.generateStaticPassword(
passwordMaxLength, _keyboardLayout);
setState(() {
_validatePassword = false;
_passwordController.text = password;
});
},
),
if (_validatePassword) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined),
labelText: l10n.s_password,
errorText: _validatePassword &&
!passwordLengthValid &&
passwordFormatValid
? l10n.s_invalid_length
: _validatePassword &&
passwordLengthValid &&
!passwordFormatValid
? l10n.l_invalid_keyboard_character
: null),
border: const OutlineInputBorder(),
labelText: l10n.s_password,
errorText: _validatePassword && !passwordLengthValid
? l10n.s_invalid_length
: _validatePassword && !passwordFormatValid
? l10n.l_invalid_keyboard_character
: null,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_generate_passowrd,
icon: const Icon(Icons.refresh),
onPressed: () async {
final password = await ref
.read(otpStateProvider(widget.devicePath).notifier)
.generateStaticPassword(
passwordMaxLength, _keyboardLayout);
setState(() {
_validatePassword = false;
_passwordController.text = password;
});
},
),
if (_validatePassword) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {

View File

@ -205,39 +205,40 @@ class _ConfigureYubiOtpDialogState
autofillHints: isAndroid ? [] : const [AutofillHints.password],
maxLength: publicIdLength,
decoration: InputDecoration(
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_use_serial,
icon: const Icon(Icons.auto_awesome_outlined),
onPressed: (info?.serial != null)
? () async {
final publicId = await ref
.read(otpStateProvider(widget.devicePath)
.notifier)
.modhexEncodeSerial(info!.serial!);
setState(() {
_publicIdController.text = publicId;
});
}
: null,
),
if (_validatePublicIdFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.public_outlined),
errorText: _validatePublicIdFormat && !publicIdFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.modhex.allowedCharacters)
: null,
labelText: l10n.s_public_id),
border: const OutlineInputBorder(),
labelText: l10n.s_public_id,
errorText: _validatePublicIdFormat && !publicIdFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.modhex.allowedCharacters)
: null,
prefixIcon: const Icon(Icons.public_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_use_serial,
icon: const Icon(Icons.auto_awesome_outlined),
onPressed: (info?.serial != null)
? () async {
final publicId = await ref
.read(otpStateProvider(widget.devicePath)
.notifier)
.modhexEncodeSerial(info!.serial!);
setState(() {
_publicIdController.text = publicId;
});
}
: null,
),
if (_validatePublicIdFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
@ -251,40 +252,41 @@ class _ConfigureYubiOtpDialogState
autofillHints: isAndroid ? [] : const [AutofillHints.password],
maxLength: privateIdLength,
decoration: InputDecoration(
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_generate_private_id,
icon: const Icon(Icons.refresh),
onPressed: () {
final random = Random.secure();
final key = List.generate(
6,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_privateIdController.text = key;
});
},
),
if (_validatePrivateIdFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined),
errorText: _validatePrivateIdFormat && !privatedIdFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
labelText: l10n.s_private_id),
border: const OutlineInputBorder(),
labelText: l10n.s_private_id,
errorText: _validatePrivateIdFormat && !privatedIdFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_generate_random,
icon: const Icon(Icons.refresh),
onPressed: () {
final random = Random.secure();
final key = List.generate(
6,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_privateIdController.text = key;
});
},
),
if (_validatePrivateIdFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
@ -298,40 +300,41 @@ class _ConfigureYubiOtpDialogState
autofillHints: isAndroid ? [] : const [AutofillHints.password],
maxLength: secretLength,
decoration: InputDecoration(
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_generate_secret_key,
icon: const Icon(Icons.refresh),
onPressed: () {
final random = Random.secure();
final key = List.generate(
16,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_secretController.text = key;
});
},
),
if (_validateSecretFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined),
errorText: _validateSecretFormat && !secretFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
labelText: l10n.s_secret_key),
border: const OutlineInputBorder(),
labelText: l10n.s_secret_key,
errorText: _validateSecretFormat && !secretFormatValid
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
tooltip: l10n.s_generate_random,
icon: const Icon(Icons.refresh),
onPressed: () {
final random = Random.secure();
final key = List.generate(
16,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_secretController.text = key;
});
},
),
if (_validateSecretFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {

View File

@ -15,9 +15,9 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/core/models.dart';
import '../../app/models.dart';
import '../../exception/cancellation_exception.dart';
@ -40,6 +40,7 @@ class AuthenticationDialog extends ConsumerStatefulWidget {
class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
bool _defaultKeyUsed = false;
bool _keyIsWrong = false;
bool _keyFormatInvalid = false;
final _keyController = TextEditingController();
@override
@ -56,6 +57,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
ManagementKeyType.tdes)
.keyLength *
2;
final keyFormatInvalid = !Format.hex.isValid(_keyController.text);
return ResponsiveDialog(
title: Text(l10n.l_unlock_piv_management),
actions: [
@ -63,6 +65,12 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
key: keys.unlockButton,
onPressed: _keyController.text.length == keyLen
? () async {
if (keyFormatInvalid) {
setState(() {
_keyFormatInvalid = true;
});
return;
}
final navigator = Navigator.of(context);
try {
final status = await ref
@ -99,42 +107,59 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
autofocus: true,
autofillHints: const [AutofillHints.password],
controller: _keyController,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp('[a-f0-9]', caseSensitive: false))
],
readOnly: _defaultKeyUsed,
maxLength: !_defaultKeyUsed ? keyLen : null,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_management_key,
prefixIcon: const Icon(Icons.key_outlined),
errorText: _keyIsWrong ? l10n.l_wrong_key : null,
errorMaxLines: 3,
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
suffixIcon: hasMetadata
errorText: _keyIsWrong
? l10n.l_wrong_key
: _keyFormatInvalid
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: hasMetadata && (!_keyIsWrong && !_keyFormatInvalid)
? null
: IconButton(
icon: Icon(_defaultKeyUsed
? Icons.auto_awesome
: Icons.auto_awesome_outlined),
tooltip: l10n.s_use_default,
onPressed: () {
setState(() {
_defaultKeyUsed = !_defaultKeyUsed;
if (_defaultKeyUsed) {
_keyController.text = defaultManagementKey;
} else {
_keyController.clear();
}
});
},
),
: hasMetadata && (_keyIsWrong || _keyFormatInvalid)
? const Icon(Icons.error)
: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(_defaultKeyUsed
? Icons.auto_awesome
: Icons.auto_awesome_outlined),
tooltip: l10n.s_use_default,
onPressed: () {
setState(() {
_keyFormatInvalid = false;
_defaultKeyUsed = !_defaultKeyUsed;
if (_defaultKeyUsed) {
_keyController.text =
defaultManagementKey;
} else {
_keyController.clear();
}
});
},
),
if (_keyIsWrong || _keyFormatInvalid) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_keyIsWrong = false;
_keyFormatInvalid = false;
});
},
),

View File

@ -162,11 +162,15 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
autofocus: true,
key: keys.subjectField,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_subject,
errorText: _subject.isNotEmpty && _invalidSubject
? l10n.l_rfc4514_invalid
: null),
border: const OutlineInputBorder(),
labelText: l10n.s_subject,
errorText: _subject.isNotEmpty && _invalidSubject
? l10n.l_rfc4514_invalid
: null,
suffixIcon: _subject.isNotEmpty && _invalidSubject
? const Icon(Icons.error)
: null,
),
textInputAction: TextInputAction.next,
enabled: !_generating,
onChanged: (value) {

View File

@ -129,11 +129,13 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
autofillHints: const [AutofillHints.password],
key: keys.managementKeyField,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_password,
prefixIcon: const Icon(Icons.password_outlined),
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
errorMaxLines: 3),
border: const OutlineInputBorder(),
labelText: l10n.s_password,
errorText: _passwordIsWrong ? l10n.s_wrong_password : null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.password_outlined),
suffixIcon: _passwordIsWrong ? const Icon(Icons.error) : null,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {

View File

@ -17,9 +17,9 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/core/models.dart';
import '../../app/message.dart';
import '../../app/models.dart';
@ -49,6 +49,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
late bool _usesStoredKey;
late bool _storeKey;
bool _currentIsWrong = false;
bool _currentInvalidFormat = false;
bool _newInvalidFormat = false;
int _attemptsRemaining = -1;
ManagementKeyType _keyType = ManagementKeyType.tdes;
final _currentController = TextEditingController();
@ -76,6 +78,16 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
}
_submit() async {
final currentInvalidFormat = Format.hex.isValid(_currentController.text);
final newInvalidFormat = Format.hex.isValid(_keyController.text);
if (!currentInvalidFormat || !newInvalidFormat) {
setState(() {
_currentInvalidFormat = !currentInvalidFormat;
_newInvalidFormat = !newInvalidFormat;
});
return;
}
final notifier = ref.read(pivStateProvider(widget.path).notifier);
if (_usesStoredKey) {
final status = (await notifier.verifyPin(_currentController.text)).when(
@ -161,18 +173,25 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
maxLength: 8,
controller: _currentController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_pin,
prefixIcon: const Icon(Icons.pin_outlined),
errorText: _currentIsWrong
? l10n
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
: null,
errorMaxLines: 3),
border: const OutlineInputBorder(),
labelText: l10n.s_pin,
errorText: _currentIsWrong
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
: _currentInvalidFormat
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.pin_outlined),
suffixIcon: _currentIsWrong || _currentInvalidFormat
? const Icon(Icons.error)
: null,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_currentIsWrong = false;
_currentInvalidFormat = false;
});
},
),
@ -187,33 +206,52 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_current_management_key,
prefixIcon: const Icon(Icons.key_outlined),
errorText: _currentIsWrong ? l10n.l_wrong_key : null,
errorMaxLines: 3,
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
suffixIcon: _hasMetadata
errorText: _currentIsWrong
? l10n.l_wrong_key
: _currentInvalidFormat
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: (_hasMetadata &&
!_currentIsWrong &&
!_currentInvalidFormat)
? null
: IconButton(
icon: Icon(_defaultKeyUsed
? Icons.auto_awesome
: Icons.auto_awesome_outlined),
tooltip: l10n.s_use_default,
onPressed: () {
setState(() {
_defaultKeyUsed = !_defaultKeyUsed;
if (_defaultKeyUsed) {
_currentController.text = defaultManagementKey;
} else {
_currentController.clear();
}
});
},
),
: (_hasMetadata && _currentIsWrong ||
_currentInvalidFormat)
? const Icon(Icons.error)
: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(_defaultKeyUsed
? Icons.auto_awesome
: Icons.auto_awesome_outlined),
tooltip: l10n.s_use_default,
onPressed: () {
setState(() {
_defaultKeyUsed = !_defaultKeyUsed;
if (_defaultKeyUsed) {
_currentController.text =
defaultManagementKey;
} else {
_currentController.clear();
}
});
},
),
if (_currentIsWrong ||
_currentInvalidFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp('[a-f0-9]', caseSensitive: false))
],
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
@ -227,33 +265,44 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
autofillHints: const [AutofillHints.newPassword],
maxLength: hexLength,
controller: _keyController,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp('[a-f0-9]', caseSensitive: false))
],
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_new_management_key,
prefixIcon: const Icon(Icons.key_outlined),
errorText: _newInvalidFormat
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
enabled: currentLenOk,
suffixIcon: IconButton(
key: keys.managementKeyRefresh,
icon: const Icon(Icons.refresh),
tooltip: l10n.s_generate_random,
onPressed: currentLenOk
? () {
final random = Random.secure();
final key = List.generate(
_keyType.keyLength,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_keyController.text = key;
});
}
: null,
prefixIcon: const Icon(Icons.key_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: l10n.s_generate_random,
onPressed: currentLenOk
? () {
final random = Random.secure();
final key = List.generate(
_keyType.keyLength,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_keyController.text = key;
_newInvalidFormat = false;
});
}
: null,
),
if (_newInvalidFormat) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,

View File

@ -109,19 +109,21 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
autofillHints: const [AutofillHints.password],
key: keys.pinPukField,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: widget.target == ManageTarget.pin
? l10n.s_current_pin
: l10n.s_current_puk,
prefixIcon: const Icon(Icons.password_outlined),
errorText: _currentIsWrong
? (widget.target == ManageTarget.pin
? l10n.l_wrong_pin_attempts_remaining(
_attemptsRemaining)
: l10n.l_wrong_puk_attempts_remaining(
_attemptsRemaining))
: null,
errorMaxLines: 3),
border: const OutlineInputBorder(),
labelText: widget.target == ManageTarget.pin
? l10n.s_current_pin
: l10n.s_current_puk,
errorText: _currentIsWrong
? (widget.target == ManageTarget.pin
? l10n
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
: l10n
.l_wrong_puk_attempts_remaining(_attemptsRemaining))
: null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.password_outlined),
suffixIcon: _currentIsWrong ? const Icon(Icons.error) : null,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {

View File

@ -96,22 +96,34 @@ class _PinDialogState extends ConsumerState<PinDialog> {
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: l10n.s_pin,
prefixIcon: const Icon(Icons.pin_outlined),
errorText: _pinIsWrong
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
: null,
errorMaxLines: 3,
suffixIcon: IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
color: IconTheme.of(context).color,
),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin,
prefixIcon: const Icon(Icons.pin_outlined),
suffixIcon: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off,
color: !_pinIsWrong
? IconTheme.of(context).color
: null),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin,
),
if (_pinIsWrong) ...[
const Icon(Icons.error_outlined),
const SizedBox(
width: 8.0,
)
]
],
),
),
textInputAction: TextInputAction.next,