mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-23 00:57:26 +03:00
Improve PIN handling consistency between views
This commit is contained in:
parent
0f76aa5b1d
commit
3245c0d637
@ -44,7 +44,8 @@ class FidoPinDialog extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||||
String _currentPin = '';
|
final _currentPinController = TextEditingController();
|
||||||
|
final _currentPinFocus = FocusNode();
|
||||||
String _newPin = '';
|
String _newPin = '';
|
||||||
String _confirmPin = '';
|
String _confirmPin = '';
|
||||||
String? _currentPinError;
|
String? _currentPinError;
|
||||||
@ -54,15 +55,24 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
bool _isObscureCurrent = true;
|
bool _isObscureCurrent = true;
|
||||||
bool _isObscureNew = true;
|
bool _isObscureNew = true;
|
||||||
bool _isObscureConfirm = true;
|
bool _isObscureConfirm = true;
|
||||||
|
bool _isBlocked = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_currentPinController.dispose();
|
||||||
|
_currentPinFocus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final hasPin = widget.state.hasPin;
|
final hasPin = widget.state.hasPin;
|
||||||
final isValid = _newPin.isNotEmpty &&
|
|
||||||
_newPin == _confirmPin &&
|
|
||||||
(!hasPin || _currentPin.isNotEmpty);
|
|
||||||
final minPinLength = widget.state.minPinLength;
|
final minPinLength = widget.state.minPinLength;
|
||||||
|
// N.B. current PIN may be shorter than minimum, if it was set before the minimum was increased
|
||||||
|
final currentPinLenOk = !hasPin || _currentPinController.text.length >= 4;
|
||||||
|
final newPinLenOk = _newPin.length >= minPinLength;
|
||||||
|
final isValid = currentPinLenOk && newPinLenOk && _newPin == _confirmPin;
|
||||||
|
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin),
|
title: Text(hasPin ? l10n.s_change_pin : l10n.s_set_pin),
|
||||||
@ -82,11 +92,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
Text(l10n.p_enter_current_pin_or_reset_no_puk),
|
Text(l10n.p_enter_current_pin_or_reset_no_puk),
|
||||||
AppTextFormField(
|
AppTextFormField(
|
||||||
key: currentPin,
|
key: currentPin,
|
||||||
initialValue: _currentPin,
|
controller: _currentPinController,
|
||||||
|
focusNode: _currentPinFocus,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: _isObscureCurrent,
|
obscureText: _isObscureCurrent,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
|
enabled: !_isBlocked,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_current_pin,
|
labelText: l10n.s_current_pin,
|
||||||
errorText: _currentIsWrong ? _currentPinError : null,
|
errorText: _currentIsWrong ? _currentPinError : null,
|
||||||
@ -108,7 +120,6 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = false;
|
_currentIsWrong = false;
|
||||||
_currentPin = value;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -124,7 +135,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_new_pin,
|
labelText: l10n.s_new_pin,
|
||||||
enabled: !hasPin || _currentPin.isNotEmpty,
|
enabled: !_isBlocked && currentPinLenOk,
|
||||||
errorText: _newIsWrong ? _newPinError : null,
|
errorText: _newIsWrong ? _newPinError : null,
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
prefixIcon: const Icon(Symbols.pin),
|
prefixIcon: const Icon(Symbols.pin),
|
||||||
@ -168,8 +179,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
tooltip:
|
tooltip:
|
||||||
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
|
_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||||
),
|
),
|
||||||
enabled:
|
enabled: !_isBlocked && currentPinLenOk && newPinLenOk,
|
||||||
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
|
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -196,7 +206,10 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
void _submit() async {
|
void _submit() async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final minPinLength = widget.state.minPinLength;
|
final minPinLength = widget.state.minPinLength;
|
||||||
final oldPin = _currentPin.isNotEmpty ? _currentPin : null;
|
final oldPin = _currentPinController.text.isNotEmpty
|
||||||
|
? _currentPinController.text
|
||||||
|
: null;
|
||||||
|
// TODO: Remove this? It shouldn't happen...
|
||||||
if (_newPin.length < minPinLength) {
|
if (_newPin.length < minPinLength) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_newPinError = l10n.l_new_pin_len(minPinLength);
|
_newPinError = l10n.l_new_pin_len(minPinLength);
|
||||||
@ -213,9 +226,13 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
|||||||
showMessage(context, l10n.s_pin_set);
|
showMessage(context, l10n.s_pin_set);
|
||||||
}, failed: (retries, authBlocked) {
|
}, failed: (retries, authBlocked) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_currentPinController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _currentPinController.text.length);
|
||||||
|
_currentPinFocus.requestFocus();
|
||||||
if (authBlocked) {
|
if (authBlocked) {
|
||||||
_currentPinError = l10n.l_pin_soft_locked;
|
_currentPinError = l10n.l_pin_soft_locked;
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
|
_isBlocked = true;
|
||||||
} else {
|
} else {
|
||||||
_currentPinError = l10n.l_wrong_pin_attempts_remaining(retries);
|
_currentPinError = l10n.l_wrong_pin_attempts_remaining(retries);
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
|
@ -37,11 +37,19 @@ class PinEntryForm extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
||||||
final _pinController = TextEditingController();
|
final _pinController = TextEditingController();
|
||||||
|
final _pinFocus = FocusNode();
|
||||||
bool _blocked = false;
|
bool _blocked = false;
|
||||||
int? _retries;
|
int? _retries;
|
||||||
bool _pinIsWrong = false;
|
bool _pinIsWrong = false;
|
||||||
bool _isObscure = true;
|
bool _isObscure = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pinController.dispose();
|
||||||
|
_pinFocus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _submit() async {
|
void _submit() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pinIsWrong = false;
|
_pinIsWrong = false;
|
||||||
@ -51,8 +59,10 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
|||||||
.read(fidoStateProvider(widget._deviceNode.path).notifier)
|
.read(fidoStateProvider(widget._deviceNode.path).notifier)
|
||||||
.unlock(_pinController.text);
|
.unlock(_pinController.text);
|
||||||
result.whenOrNull(failed: (retries, authBlocked) {
|
result.whenOrNull(failed: (retries, authBlocked) {
|
||||||
|
_pinController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _pinController.text.length);
|
||||||
|
_pinFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_pinController.clear();
|
|
||||||
_pinIsWrong = true;
|
_pinIsWrong = true;
|
||||||
_retries = retries;
|
_retries = retries;
|
||||||
_blocked = authBlocked;
|
_blocked = authBlocked;
|
||||||
@ -92,6 +102,8 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
|||||||
obscureText: _isObscure,
|
obscureText: _isObscure,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
controller: _pinController,
|
controller: _pinController,
|
||||||
|
focusNode: _pinFocus,
|
||||||
|
enabled: !_blocked && (_retries ?? 1) > 0,
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_pin,
|
labelText: l10n.s_pin,
|
||||||
@ -137,7 +149,9 @@ class _PinEntryFormState extends ConsumerState<PinEntryForm> {
|
|||||||
icon: const Icon(Symbols.lock_open),
|
icon: const Icon(Symbols.lock_open),
|
||||||
label: Text(l10n.s_unlock),
|
label: Text(l10n.s_unlock),
|
||||||
onPressed:
|
onPressed:
|
||||||
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
!_pinIsWrong && _pinController.text.length >= 4 && !_blocked
|
||||||
|
? _submit
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -41,7 +41,8 @@ class ManagePasswordDialog extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||||
String _currentPassword = '';
|
final _currentPasswordController = TextEditingController();
|
||||||
|
final _currentPasswordFocus = FocusNode();
|
||||||
String _newPassword = '';
|
String _newPassword = '';
|
||||||
String _confirmPassword = '';
|
String _confirmPassword = '';
|
||||||
bool _currentIsWrong = false;
|
bool _currentIsWrong = false;
|
||||||
@ -49,12 +50,19 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
bool _isObscureNew = true;
|
bool _isObscureNew = true;
|
||||||
bool _isObscureConfirm = true;
|
bool _isObscureConfirm = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_currentPasswordController.dispose();
|
||||||
|
_currentPasswordFocus.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
_submit() async {
|
_submit() async {
|
||||||
FocusUtils.unfocus(context);
|
FocusUtils.unfocus(context);
|
||||||
|
|
||||||
final result = await ref
|
final result = await ref
|
||||||
.read(oathStateProvider(widget.path).notifier)
|
.read(oathStateProvider(widget.path).notifier)
|
||||||
.setPassword(_currentPassword, _newPassword);
|
.setPassword(_currentPasswordController.text, _newPassword);
|
||||||
if (result) {
|
if (result) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
await ref.read(withContextProvider)((context) async {
|
await ref.read(withContextProvider)((context) async {
|
||||||
@ -63,6 +71,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
_currentPasswordController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _currentPasswordController.text.length);
|
||||||
|
_currentPasswordFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
});
|
});
|
||||||
@ -72,9 +83,10 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final isValid = _newPassword.isNotEmpty &&
|
final isValid = !_currentIsWrong &&
|
||||||
|
_newPassword.isNotEmpty &&
|
||||||
_newPassword == _confirmPassword &&
|
_newPassword == _confirmPassword &&
|
||||||
(!widget.state.hasKey || _currentPassword.isNotEmpty);
|
(!widget.state.hasKey || _currentPasswordController.text.isNotEmpty);
|
||||||
|
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -98,6 +110,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
obscureText: _isObscureCurrent,
|
obscureText: _isObscureCurrent,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.currentPasswordField,
|
key: keys.currentPasswordField,
|
||||||
|
controller: _currentPasswordController,
|
||||||
|
focusNode: _currentPasswordFocus,
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_current_password,
|
labelText: l10n.s_current_password,
|
||||||
@ -121,7 +135,6 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = false;
|
_currentIsWrong = false;
|
||||||
_currentPassword = value;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -131,11 +144,12 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
children: [
|
children: [
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
key: keys.removePasswordButton,
|
key: keys.removePasswordButton,
|
||||||
onPressed: _currentPassword.isNotEmpty
|
onPressed: _currentPasswordController.text.isNotEmpty &&
|
||||||
|
!_currentIsWrong
|
||||||
? () async {
|
? () async {
|
||||||
final result = await ref
|
final result = await ref
|
||||||
.read(oathStateProvider(widget.path).notifier)
|
.read(oathStateProvider(widget.path).notifier)
|
||||||
.unsetPassword(_currentPassword);
|
.unsetPassword(_currentPasswordController.text);
|
||||||
if (result) {
|
if (result) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
await ref.read(withContextProvider)(
|
await ref.read(withContextProvider)(
|
||||||
@ -145,6 +159,12 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
_currentPasswordController.selection =
|
||||||
|
TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: _currentPasswordController
|
||||||
|
.text.length);
|
||||||
|
_currentPasswordFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
});
|
});
|
||||||
@ -193,7 +213,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
tooltip: _isObscureNew
|
tooltip: _isObscureNew
|
||||||
? l10n.s_show_password
|
? l10n.s_show_password
|
||||||
: l10n.s_hide_password),
|
: l10n.s_hide_password),
|
||||||
enabled: !widget.state.hasKey || _currentPassword.isNotEmpty,
|
enabled: !widget.state.hasKey ||
|
||||||
|
_currentPasswordController.text.isNotEmpty,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -227,9 +248,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
|||||||
tooltip: _isObscureConfirm
|
tooltip: _isObscureConfirm
|
||||||
? l10n.s_show_password
|
? l10n.s_show_password
|
||||||
: l10n.s_hide_password),
|
: l10n.s_hide_password),
|
||||||
enabled:
|
enabled: (!widget.state.hasKey ||
|
||||||
(!widget.state.hasKey || _currentPassword.isNotEmpty) &&
|
_currentPasswordController.text.isNotEmpty) &&
|
||||||
_newPassword.isNotEmpty,
|
_newPassword.isNotEmpty,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
@ -38,6 +38,7 @@ class UnlockForm extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _UnlockFormState extends ConsumerState<UnlockForm> {
|
class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
|
final _passwordFocus = FocusNode();
|
||||||
bool _remember = false;
|
bool _remember = false;
|
||||||
bool _passwordIsWrong = false;
|
bool _passwordIsWrong = false;
|
||||||
bool _isObscure = true;
|
bool _isObscure = true;
|
||||||
@ -51,9 +52,11 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
|||||||
.unlock(_passwordController.text, remember: _remember);
|
.unlock(_passwordController.text, remember: _remember);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
_passwordController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _passwordController.text.length);
|
||||||
|
_passwordFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_passwordIsWrong = true;
|
_passwordIsWrong = true;
|
||||||
_passwordController.clear();
|
|
||||||
});
|
});
|
||||||
} else if (_remember && !remembered) {
|
} else if (_remember && !remembered) {
|
||||||
showMessage(context, AppLocalizations.of(context)!.l_remember_pw_failed);
|
showMessage(context, AppLocalizations.of(context)!.l_remember_pw_failed);
|
||||||
@ -79,6 +82,7 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
|||||||
child: AppTextField(
|
child: AppTextField(
|
||||||
key: keys.passwordField,
|
key: keys.passwordField,
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
|
focusNode: _passwordFocus,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: _isObscure,
|
obscureText: _isObscure,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
|
@ -44,10 +44,12 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
bool _keyIsWrong = false;
|
bool _keyIsWrong = false;
|
||||||
bool _keyFormatInvalid = false;
|
bool _keyFormatInvalid = false;
|
||||||
final _keyController = TextEditingController();
|
final _keyController = TextEditingController();
|
||||||
|
final _keyFocus = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_keyController.dispose();
|
_keyController.dispose();
|
||||||
|
_keyFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +67,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
key: keys.unlockButton,
|
key: keys.unlockButton,
|
||||||
onPressed: _keyController.text.length == keyLen
|
onPressed: !_keyIsWrong && _keyController.text.length == keyLen
|
||||||
? () async {
|
? () async {
|
||||||
if (keyFormatInvalid) {
|
if (keyFormatInvalid) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -81,6 +83,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
if (status) {
|
if (status) {
|
||||||
navigator.pop(true);
|
navigator.pop(true);
|
||||||
} else {
|
} else {
|
||||||
|
_keyController.selection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: _keyController.text.length);
|
||||||
|
_keyFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_keyIsWrong = true;
|
_keyIsWrong = true;
|
||||||
});
|
});
|
||||||
@ -88,6 +94,10 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
} on CancellationException catch (_) {
|
} on CancellationException catch (_) {
|
||||||
navigator.pop(false);
|
navigator.pop(false);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
_keyController.selection = TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: _keyController.text.length);
|
||||||
|
_keyFocus.requestFocus();
|
||||||
// TODO: More error cases
|
// TODO: More error cases
|
||||||
setState(() {
|
setState(() {
|
||||||
_keyIsWrong = true;
|
_keyIsWrong = true;
|
||||||
@ -109,6 +119,7 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
autofocus: true,
|
autofocus: true,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
controller: _keyController,
|
controller: _keyController,
|
||||||
|
focusNode: _keyFocus,
|
||||||
readOnly: _defaultKeyUsed,
|
readOnly: _defaultKeyUsed,
|
||||||
maxLength: !_defaultKeyUsed ? keyLen : null,
|
maxLength: !_defaultKeyUsed ? keyLen : null,
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
|
@ -57,6 +57,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
int _attemptsRemaining = -1;
|
int _attemptsRemaining = -1;
|
||||||
late ManagementKeyType _keyType;
|
late ManagementKeyType _keyType;
|
||||||
final _currentController = TextEditingController();
|
final _currentController = TextEditingController();
|
||||||
|
final _currentFocus = FocusNode();
|
||||||
final _keyController = TextEditingController();
|
final _keyController = TextEditingController();
|
||||||
bool _isObscure = true;
|
bool _isObscure = true;
|
||||||
|
|
||||||
@ -84,6 +85,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_keyController.dispose();
|
_keyController.dispose();
|
||||||
_currentController.dispose();
|
_currentController.dispose();
|
||||||
|
_currentFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +105,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
final status = (await notifier.verifyPin(_currentController.text)).when(
|
final status = (await notifier.verifyPin(_currentController.text)).when(
|
||||||
success: () => true,
|
success: () => true,
|
||||||
failure: (attemptsRemaining) {
|
failure: (attemptsRemaining) {
|
||||||
|
_currentController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _currentController.text.length);
|
||||||
|
_currentFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_attemptsRemaining = attemptsRemaining;
|
_attemptsRemaining = attemptsRemaining;
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
@ -115,6 +120,9 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!await notifier.authenticate(_currentController.text)) {
|
if (!await notifier.authenticate(_currentController.text)) {
|
||||||
|
_currentController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _currentController.text.length);
|
||||||
|
_currentFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
});
|
});
|
||||||
@ -166,7 +174,8 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
title: Text(l10n.l_change_management_key),
|
title: Text(l10n.l_change_management_key),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: currentLenOk && newLenOk ? _submit : null,
|
onPressed:
|
||||||
|
!_currentIsWrong && currentLenOk && newLenOk ? _submit : null,
|
||||||
key: keys.saveButton,
|
key: keys.saveButton,
|
||||||
child: Text(l10n.s_save),
|
child: Text(l10n.s_save),
|
||||||
)
|
)
|
||||||
@ -185,6 +194,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
key: keys.pinPukField,
|
key: keys.pinPukField,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
controller: _currentController,
|
controller: _currentController,
|
||||||
|
focusNode: _currentFocus,
|
||||||
readOnly: _defaultPinUsed,
|
readOnly: _defaultPinUsed,
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
@ -223,6 +233,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
autofocus: !_defaultKeyUsed,
|
autofocus: !_defaultKeyUsed,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
controller: _currentController,
|
controller: _currentController,
|
||||||
|
focusNode: _currentFocus,
|
||||||
readOnly: _defaultKeyUsed,
|
readOnly: _defaultKeyUsed,
|
||||||
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
|
@ -44,20 +44,25 @@ class ManagePinPukDialog extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
||||||
final _currentPinController = TextEditingController();
|
final _currentPinController = TextEditingController();
|
||||||
|
final _currentPinFocus = FocusNode();
|
||||||
String _newPin = '';
|
String _newPin = '';
|
||||||
String _confirmPin = '';
|
String _confirmPin = '';
|
||||||
|
bool _pinIsBlocked = false;
|
||||||
bool _currentIsWrong = false;
|
bool _currentIsWrong = false;
|
||||||
int _attemptsRemaining = -1;
|
int _attemptsRemaining = -1;
|
||||||
bool _isObscureCurrent = true;
|
bool _isObscureCurrent = true;
|
||||||
bool _isObscureNew = true;
|
bool _isObscureNew = true;
|
||||||
bool _isObscureConfirm = true;
|
bool _isObscureConfirm = true;
|
||||||
late bool _defaultPinUsed;
|
late final bool _defaultPinUsed;
|
||||||
late bool _defaultPukUsed;
|
late final bool _defaultPukUsed;
|
||||||
|
late final int _minPinLen;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// Old YubiKeys allowed a 4 digit PIN
|
||||||
|
_minPinLen = widget.pivState.version.isAtLeast(4, 3, 1) ? 6 : 4;
|
||||||
_defaultPinUsed =
|
_defaultPinUsed =
|
||||||
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
|
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
|
||||||
_defaultPukUsed =
|
_defaultPukUsed =
|
||||||
@ -73,6 +78,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_currentPinController.dispose();
|
_currentPinController.dispose();
|
||||||
|
_currentPinFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,11 +104,16 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
_ => l10n.s_pin_set,
|
_ => l10n.s_pin_set,
|
||||||
});
|
});
|
||||||
}, failure: (attemptsRemaining) {
|
}, failure: (attemptsRemaining) {
|
||||||
|
_currentPinController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _currentPinController.text.length);
|
||||||
|
_currentPinFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_attemptsRemaining = attemptsRemaining;
|
_attemptsRemaining = attemptsRemaining;
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
|
if (_attemptsRemaining == 0) {
|
||||||
|
_pinIsBlocked = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
_currentPinController.clear();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +121,10 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final currentPin = _currentPinController.text;
|
final currentPin = _currentPinController.text;
|
||||||
final isValid =
|
final isValid = !_currentIsWrong &&
|
||||||
_newPin.isNotEmpty && _newPin == _confirmPin && currentPin.isNotEmpty;
|
_newPin.isNotEmpty &&
|
||||||
|
_newPin == _confirmPin &&
|
||||||
|
currentPin.isNotEmpty;
|
||||||
|
|
||||||
final titleText = switch (widget.target) {
|
final titleText = switch (widget.target) {
|
||||||
ManageTarget.pin => l10n.s_change_pin,
|
ManageTarget.pin => l10n.s_change_pin,
|
||||||
@ -138,7 +151,6 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
//TODO fix string
|
|
||||||
Text(widget.target == ManageTarget.pin
|
Text(widget.target == ManageTarget.pin
|
||||||
? l10n.p_enter_current_pin_or_reset
|
? l10n.p_enter_current_pin_or_reset
|
||||||
: l10n.p_enter_current_puk_or_reset),
|
: l10n.p_enter_current_puk_or_reset),
|
||||||
@ -150,6 +162,8 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
key: keys.pinPukField,
|
key: keys.pinPukField,
|
||||||
readOnly: showDefaultPinUsed || showDefaultPukUsed,
|
readOnly: showDefaultPinUsed || showDefaultPukUsed,
|
||||||
controller: _currentPinController,
|
controller: _currentPinController,
|
||||||
|
focusNode: _currentPinFocus,
|
||||||
|
enabled: !_pinIsBlocked,
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
helperText: showDefaultPinUsed
|
helperText: showDefaultPinUsed
|
||||||
@ -160,13 +174,17 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
labelText: widget.target == ManageTarget.pin
|
labelText: widget.target == ManageTarget.pin
|
||||||
? l10n.s_current_pin
|
? l10n.s_current_pin
|
||||||
: l10n.s_current_puk,
|
: l10n.s_current_puk,
|
||||||
errorText: _currentIsWrong
|
errorText: _pinIsBlocked
|
||||||
? (widget.target == ManageTarget.pin
|
? (widget.target == ManageTarget.pin
|
||||||
? l10n
|
? l10n.l_piv_pin_blocked
|
||||||
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
: l10n.l_piv_pin_puk_blocked)
|
||||||
: l10n
|
: (_currentIsWrong
|
||||||
.l_wrong_puk_attempts_remaining(_attemptsRemaining))
|
? (widget.target == ManageTarget.pin
|
||||||
: null,
|
? l10n.l_wrong_pin_attempts_remaining(
|
||||||
|
_attemptsRemaining)
|
||||||
|
: l10n.l_wrong_puk_attempts_remaining(
|
||||||
|
_attemptsRemaining))
|
||||||
|
: null),
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
prefixIcon: const Icon(Symbols.password),
|
prefixIcon: const Icon(Symbols.password),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
@ -217,8 +235,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
? (_isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin)
|
? (_isObscureNew ? l10n.s_show_pin : l10n.s_hide_pin)
|
||||||
: (_isObscureNew ? l10n.s_show_puk : l10n.s_hide_puk),
|
: (_isObscureNew ? l10n.s_show_puk : l10n.s_hide_puk),
|
||||||
),
|
),
|
||||||
// Old YubiKeys allowed a 4 digit PIN
|
enabled: currentPin.length >= _minPinLen,
|
||||||
enabled: currentPin.length >= 4,
|
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@ -256,7 +273,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
? (_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin)
|
? (_isObscureConfirm ? l10n.s_show_pin : l10n.s_hide_pin)
|
||||||
: (_isObscureConfirm ? l10n.s_show_puk : l10n.s_hide_puk),
|
: (_isObscureConfirm ? l10n.s_show_puk : l10n.s_hide_puk),
|
||||||
),
|
),
|
||||||
enabled: currentPin.length >= 4 && _newPin.length >= 6,
|
enabled: currentPin.length >= _minPinLen && _newPin.length >= 6,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
@ -37,6 +37,7 @@ class PinDialog extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _PinDialogState extends ConsumerState<PinDialog> {
|
class _PinDialogState extends ConsumerState<PinDialog> {
|
||||||
final _pinController = TextEditingController();
|
final _pinController = TextEditingController();
|
||||||
|
final _pinFocus = FocusNode();
|
||||||
bool _pinIsWrong = false;
|
bool _pinIsWrong = false;
|
||||||
int _attemptsRemaining = -1;
|
int _attemptsRemaining = -1;
|
||||||
bool _isObscure = true;
|
bool _isObscure = true;
|
||||||
@ -44,6 +45,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pinController.dispose();
|
_pinController.dispose();
|
||||||
|
_pinFocus.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +60,10 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
navigator.pop(true);
|
navigator.pop(true);
|
||||||
},
|
},
|
||||||
failure: (attemptsRemaining) {
|
failure: (attemptsRemaining) {
|
||||||
|
_pinController.selection = TextSelection(
|
||||||
|
baseOffset: 0, extentOffset: _pinController.text.length);
|
||||||
|
_pinFocus.requestFocus();
|
||||||
setState(() {
|
setState(() {
|
||||||
_pinController.clear();
|
|
||||||
_attemptsRemaining = attemptsRemaining;
|
_attemptsRemaining = attemptsRemaining;
|
||||||
_pinIsWrong = true;
|
_pinIsWrong = true;
|
||||||
});
|
});
|
||||||
@ -73,12 +77,14 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final version = ref.watch(pivStateProvider(widget.devicePath)).valueOrNull;
|
||||||
|
final minPinLen = version?.version.isAtLeast(4, 3, 1) == true ? 6 : 4;
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
title: Text(l10n.s_pin_required),
|
title: Text(l10n.s_pin_required),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
key: keys.unlockButton,
|
key: keys.unlockButton,
|
||||||
onPressed: _pinController.text.length >= 4 ? _submit : null,
|
onPressed: _pinController.text.length >= minPinLen ? _submit : null,
|
||||||
child: Text(l10n.s_unlock),
|
child: Text(l10n.s_unlock),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -95,6 +101,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.managementKeyField,
|
key: keys.managementKeyField,
|
||||||
controller: _pinController,
|
controller: _pinController,
|
||||||
|
focusNode: _pinFocus,
|
||||||
decoration: AppInputDecoration(
|
decoration: AppInputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_pin,
|
labelText: l10n.s_pin,
|
||||||
|
Loading…
Reference in New Issue
Block a user