mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 23:14:18 +03:00
fixes for password and pin textinput handling and some changes for the password/pin management dialogs
This commit is contained in:
parent
580e91d195
commit
751b972112
@ -46,7 +46,6 @@ class FidoLockedPage extends ConsumerWidget {
|
||||
actions: _buildActions(context),
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(title: Text('Unlock')),
|
||||
_PinEntryForm(state, node),
|
||||
],
|
||||
),
|
||||
@ -111,14 +110,21 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
final _pinController = TextEditingController();
|
||||
bool _blocked = false;
|
||||
int? _retries;
|
||||
bool _pinIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
|
||||
void _submit() async {
|
||||
setState(() {
|
||||
_pinIsWrong = false;
|
||||
_isObscure = true;
|
||||
});
|
||||
final result = await ref
|
||||
.read(fidoStateProvider(widget._deviceNode.path).notifier)
|
||||
.unlock(_pinController.text);
|
||||
result.whenOrNull(failed: (retries, authBlocked) {
|
||||
setState(() {
|
||||
_pinController.clear();
|
||||
_pinIsWrong = true;
|
||||
_retries = retries;
|
||||
_blocked = authBlocked;
|
||||
});
|
||||
@ -142,23 +148,39 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
||||
Widget build(BuildContext context) {
|
||||
final noFingerprints = widget._state.bioEnroll == false;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Enter the FIDO2 PIN for your YubiKey'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
obscureText: _isObscure,
|
||||
controller: _pinController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'PIN',
|
||||
errorText: _getErrorText(),
|
||||
helperText: '', // Prevents dialog resizing
|
||||
errorText: _pinIsWrong ? _getErrorText() : null,
|
||||
errorMaxLines: 3,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscure = !_isObscure;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {}), // Update state on change
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_pinIsWrong = false;
|
||||
});
|
||||
}, // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
),
|
||||
|
@ -23,6 +23,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
String _confirmPin = '';
|
||||
String? _currentPinError;
|
||||
String? _newPinError;
|
||||
bool _currentIsWrong = false;
|
||||
bool _newIsWrong = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -48,10 +50,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasPin) ...[
|
||||
Text(
|
||||
'Current PIN',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Text(
|
||||
"Enter your current PIN. If you don't know your PIN, you'll need to reset the YubiKey."),
|
||||
TextFormField(
|
||||
initialValue: _currentPin,
|
||||
autofocus: true,
|
||||
@ -59,19 +59,19 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Current PIN',
|
||||
errorText: _currentPinError,
|
||||
errorText: _currentIsWrong ? _currentPinError : null,
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentIsWrong = false;
|
||||
_currentPin = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
Text(
|
||||
'New PIN',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Text(
|
||||
'Enter your new PIN. A PIN must be at least 4 characters long and may contain letters, numbers and special characters.'),
|
||||
TextFormField(
|
||||
initialValue: _newPin,
|
||||
autofocus: !hasPin,
|
||||
@ -80,10 +80,12 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'New PIN',
|
||||
enabled: !hasPin || _currentPin.isNotEmpty,
|
||||
errorText: _newPinError,
|
||||
errorText: _newIsWrong ? _newPinError : null,
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_newIsWrong = false;
|
||||
_newPin = value;
|
||||
});
|
||||
},
|
||||
@ -94,7 +96,8 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Confirm PIN',
|
||||
enabled: _newPin.isNotEmpty,
|
||||
enabled:
|
||||
(!hasPin || _currentPin.isNotEmpty) && _newPin.isNotEmpty,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
@ -123,6 +126,7 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
if (_newPin.length < minPinLength) {
|
||||
setState(() {
|
||||
_newPinError = 'New PIN must be at least $minPinLength characters';
|
||||
_newIsWrong = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -137,8 +141,10 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
|
||||
if (authBlocked) {
|
||||
_currentPinError =
|
||||
'PIN has been blocked until the YubiKey is removed and reinserted';
|
||||
_currentIsWrong = true;
|
||||
} else {
|
||||
_currentPinError = 'Wrong PIN ($retries tries remaining)';
|
||||
_currentIsWrong = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -62,19 +62,19 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.state.hasKey) ...[
|
||||
Text(
|
||||
'Current password',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Text(
|
||||
"Enter your current password. If you don't know your password, you'll need to reset the YubiKey."),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Current password',
|
||||
errorText: _currentIsWrong ? 'Wrong password' : null),
|
||||
errorText: _currentIsWrong ? 'Wrong password' : null,
|
||||
errorMaxLines: 3),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentIsWrong = false;
|
||||
_currentPassword = value;
|
||||
});
|
||||
},
|
||||
@ -116,12 +116,9 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
Text(
|
||||
'New password',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Text(
|
||||
'Enter your new password. A password may contain letters, numbers and special characters.'),
|
||||
TextField(
|
||||
autofocus: !widget.state.hasKey,
|
||||
obscureText: true,
|
||||
@ -141,7 +138,8 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Confirm password',
|
||||
enabled: _newPassword.isNotEmpty,
|
||||
enabled: (!widget.state.hasKey || _currentPassword.isNotEmpty) &&
|
||||
_newPassword.isNotEmpty,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
|
@ -84,7 +84,6 @@ class _LockedView extends ConsumerWidget {
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
const ListTile(title: Text('Unlock')),
|
||||
_UnlockForm(
|
||||
devicePath,
|
||||
keystore: oathState.keystore,
|
||||
@ -209,11 +208,13 @@ class _UnlockForm extends ConsumerStatefulWidget {
|
||||
class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
final _passwordController = TextEditingController();
|
||||
bool _remember = false;
|
||||
bool _wrong = false;
|
||||
bool _passwordIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
|
||||
void _submit() async {
|
||||
setState(() {
|
||||
_wrong = false;
|
||||
_passwordIsWrong = false;
|
||||
_isObscure = false;
|
||||
});
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget._devicePath).notifier)
|
||||
@ -221,10 +222,9 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
if (!mounted) return;
|
||||
if (!result.first) {
|
||||
setState(() {
|
||||
_wrong = true;
|
||||
_passwordIsWrong = true;
|
||||
_passwordController.clear();
|
||||
});
|
||||
showMessage(context, 'Wrong password');
|
||||
} else if (_remember && !result.second) {
|
||||
showMessage(context, 'Failed to remember password');
|
||||
}
|
||||
@ -236,7 +236,7 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -247,51 +247,59 @@ class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
obscureText: _isObscure,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'Password',
|
||||
errorText: _wrong ? 'Wrong password' : null,
|
||||
errorText: _passwordIsWrong ? 'Wrong password' : null,
|
||||
helperText: '', // Prevents resizing when errorText shown
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isObscure = !_isObscure;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {}), // Update state on change
|
||||
onChanged: (_) => setState(() {
|
||||
_passwordIsWrong = false;
|
||||
}), // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: keystoreFailed
|
||||
? const ListTile(
|
||||
leading: Icon(Icons.warning_amber_rounded),
|
||||
title: Text('OS Keystore unavailable'),
|
||||
dense: true,
|
||||
minLeadingWidth: 0,
|
||||
)
|
||||
: CheckboxListTile(
|
||||
title: const Text('Remember password'),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _remember,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_remember = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
||||
child: const Text('Unlock'),
|
||||
const SizedBox(height: 8.0),
|
||||
keystoreFailed
|
||||
? const ListTile(
|
||||
leading: Icon(Icons.warning_amber_rounded),
|
||||
title: Text('OS Keystore unavailable'),
|
||||
dense: true,
|
||||
minLeadingWidth: 0,
|
||||
)
|
||||
: CheckboxListTile(
|
||||
title: const Text('Remember password'),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _remember,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_remember = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12.0, right: 18.0, bottom: 4.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
||||
child: const Text('Unlock'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user