mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Add more error handling and unlock.
This commit is contained in:
parent
84f0fe162f
commit
e243ec207e
@ -25,6 +25,7 @@ import '../../widgets/utf8_utils.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../models.dart';
|
||||
import '../state.dart';
|
||||
import 'unlock_form.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
final _log = Logger('oath.view.add_account_page');
|
||||
@ -258,7 +259,10 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
.isEmpty ??
|
||||
true;
|
||||
|
||||
final isValid = _accountController.text.trim().isNotEmpty &&
|
||||
final isLocked = oathState?.locked ?? false;
|
||||
|
||||
final isValid = !isLocked &&
|
||||
_accountController.text.trim().isNotEmpty &&
|
||||
secret.isNotEmpty &&
|
||||
isUnique &&
|
||||
issuerRemaining >= -1 &&
|
||||
@ -267,13 +271,20 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
|
||||
final qrScanner = ref.watch(qrScannerProvider);
|
||||
|
||||
final hashAlgorithms = HashAlgorithm.values.where((alg) =>
|
||||
final hashAlgorithms = HashAlgorithm.values
|
||||
.where((alg) =>
|
||||
alg != HashAlgorithm.sha512 ||
|
||||
(oathState?.version.isAtLeast(4, 3, 1) ?? true));
|
||||
(oathState?.version.isAtLeast(4, 3, 1) ?? true))
|
||||
.toList();
|
||||
if (!hashAlgorithms.contains(_hashAlgorithm)) {
|
||||
_hashAlgorithm = HashAlgorithm.sha1;
|
||||
}
|
||||
|
||||
if (!(oathState?.version.isAtLeast(4, 2) ?? true)) {
|
||||
// Touch not supported
|
||||
_touch = false;
|
||||
}
|
||||
|
||||
void submit() async {
|
||||
if (secretLengthValid) {
|
||||
final issuer = _issuerController.text.trim();
|
||||
@ -340,7 +351,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Column(
|
||||
child: isLocked
|
||||
? UnlockForm(deviceNode!.path, keystore: oathState!.keystore)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
@ -354,8 +367,10 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
buildByteCounterFor(_issuerController.text.trim()),
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_issuer_optional,
|
||||
helperText: '', // Prevents dialog resizing when disabled
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.oath_issuer_optional,
|
||||
helperText:
|
||||
'', // Prevents dialog resizing when disabled
|
||||
prefixIcon: const Icon(Icons.business_outlined),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
@ -378,8 +393,10 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.person_outline),
|
||||
labelText: AppLocalizations.of(context)!.oath_account_name,
|
||||
helperText: '', // Prevents dialog resizing when disabled
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.oath_account_name,
|
||||
helperText:
|
||||
'', // Prevents dialog resizing when disabled
|
||||
errorText: isUnique
|
||||
? null
|
||||
: AppLocalizations.of(context)!.oath_duplicate_name,
|
||||
@ -399,12 +416,15 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
controller: _secretController,
|
||||
obscureText: _isObscure,
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(_secretFormatterPattern)
|
||||
FilteringTextInputFormatter.allow(
|
||||
_secretFormatterPattern)
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||
_isObscure
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: IconTheme.of(context).color,
|
||||
),
|
||||
onPressed: () {
|
||||
@ -415,9 +435,11 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.key_outlined),
|
||||
labelText: AppLocalizations.of(context)!.oath_secret_key,
|
||||
labelText:
|
||||
AppLocalizations.of(context)!.oath_secret_key,
|
||||
errorText: _validateSecretLength && !secretLengthValid
|
||||
? AppLocalizations.of(context)!.oath_invalid_length
|
||||
? AppLocalizations.of(context)!
|
||||
.oath_invalid_length
|
||||
: null),
|
||||
readOnly: _qrState == _QrScanState.success,
|
||||
textInputAction: TextInputAction.done,
|
||||
@ -437,11 +459,15 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
avatar: _qrState != _QrScanState.scanning
|
||||
? (_qrState == _QrScanState.success
|
||||
? const Icon(Icons.qr_code)
|
||||
: const Icon(Icons.qr_code_scanner_outlined))
|
||||
: const CircularProgressIndicator(strokeWidth: 2.0),
|
||||
: const Icon(
|
||||
Icons.qr_code_scanner_outlined))
|
||||
: const CircularProgressIndicator(
|
||||
strokeWidth: 2.0),
|
||||
label: _qrState == _QrScanState.success
|
||||
? Text(AppLocalizations.of(context)!.oath_scanned_qr)
|
||||
: Text(AppLocalizations.of(context)!.oath_scan_qr),
|
||||
? Text(AppLocalizations.of(context)!
|
||||
.oath_scanned_qr)
|
||||
: Text(
|
||||
AppLocalizations.of(context)!.oath_scan_qr),
|
||||
onPressed: () {
|
||||
_scanQrCode(qrScanner);
|
||||
}),
|
||||
@ -452,10 +478,10 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
spacing: 4.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
if (widget.state?.version.isAtLeast(4, 2) ?? true)
|
||||
if (oathState?.version.isAtLeast(4, 2) ?? true)
|
||||
FilterChip(
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!.oath_require_touch),
|
||||
label: Text(AppLocalizations.of(context)!
|
||||
.oath_require_touch),
|
||||
selected: _touch,
|
||||
onSelected: (value) {
|
||||
setState(() {
|
||||
@ -477,7 +503,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
: null,
|
||||
),
|
||||
ChoiceFilterChip<HashAlgorithm>(
|
||||
items: HashAlgorithm.values,
|
||||
items: hashAlgorithms,
|
||||
value: _hashAlgorithm,
|
||||
selected: _hashAlgorithm != defaultHashAlgorithm,
|
||||
itemBuilder: (value) => Text(value.displayName),
|
||||
@ -492,10 +518,10 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
||||
if (_oathType == OathType.totp)
|
||||
ChoiceFilterChip<int>(
|
||||
items: _periodValues,
|
||||
value:
|
||||
int.tryParse(_periodController.text) ?? defaultPeriod,
|
||||
selected:
|
||||
int.tryParse(_periodController.text) != defaultPeriod,
|
||||
value: int.tryParse(_periodController.text) ??
|
||||
defaultPeriod,
|
||||
selected: int.tryParse(_periodController.text) !=
|
||||
defaultPeriod,
|
||||
itemBuilder: ((value) => Text(
|
||||
'$value ${AppLocalizations.of(context)!.oath_sec}')),
|
||||
onChanged: _qrState != _QrScanState.success
|
||||
|
@ -21,6 +21,7 @@ import 'account_list.dart';
|
||||
import 'add_account_page.dart';
|
||||
import 'manage_password_dialog.dart';
|
||||
import 'reset_dialog.dart';
|
||||
import 'unlock_form.dart';
|
||||
|
||||
class OathScreen extends ConsumerWidget {
|
||||
final DevicePath devicePath;
|
||||
@ -80,7 +81,7 @@ class _LockedView extends ConsumerWidget {
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
_UnlockForm(
|
||||
UnlockForm(
|
||||
devicePath,
|
||||
keystore: oathState.keystore,
|
||||
),
|
||||
@ -244,122 +245,3 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _UnlockForm extends ConsumerStatefulWidget {
|
||||
final DevicePath _devicePath;
|
||||
final KeystoreState keystore;
|
||||
const _UnlockForm(this._devicePath, {required this.keystore});
|
||||
|
||||
@override
|
||||
ConsumerState<_UnlockForm> createState() => _UnlockFormState();
|
||||
}
|
||||
|
||||
class _UnlockFormState extends ConsumerState<_UnlockForm> {
|
||||
final _passwordController = TextEditingController();
|
||||
bool _remember = false;
|
||||
bool _passwordIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
|
||||
void _submit() async {
|
||||
setState(() {
|
||||
_passwordIsWrong = false;
|
||||
});
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget._devicePath).notifier)
|
||||
.unlock(_passwordController.text, remember: _remember);
|
||||
if (!mounted) return;
|
||||
if (!result.first) {
|
||||
setState(() {
|
||||
_passwordIsWrong = true;
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else if (_remember && !result.second) {
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.oath_failed_remember_pw);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keystoreFailed = widget.keystore == KeystoreState.failed;
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.oath_enter_oath_pw,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
TextField(
|
||||
key: keys.passwordField,
|
||||
controller: _passwordController,
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_password,
|
||||
errorText: _passwordIsWrong
|
||||
? AppLocalizations.of(context)!.oath_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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {
|
||||
_passwordIsWrong = false;
|
||||
}), // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
keystoreFailed
|
||||
? ListTile(
|
||||
leading: const Icon(Icons.warning_amber_rounded),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.oath_keystore_unavailable),
|
||||
dense: true,
|
||||
minLeadingWidth: 0,
|
||||
)
|
||||
: CheckboxListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context)!.oath_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.icon(
|
||||
key: keys.unlockButton,
|
||||
label: Text(AppLocalizations.of(context)!.oath_unlock),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
128
lib/oath/views/unlock_form.dart
Executable file
128
lib/oath/views/unlock_form.dart
Executable file
@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/message.dart';
|
||||
import '../../app/models.dart';
|
||||
import '../models.dart';
|
||||
import '../keys.dart' as keys;
|
||||
import '../state.dart';
|
||||
|
||||
class UnlockForm extends ConsumerStatefulWidget {
|
||||
final DevicePath _devicePath;
|
||||
final KeystoreState keystore;
|
||||
const UnlockForm(this._devicePath, {required this.keystore, super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<UnlockForm> createState() => _UnlockFormState();
|
||||
}
|
||||
|
||||
class _UnlockFormState extends ConsumerState<UnlockForm> {
|
||||
final _passwordController = TextEditingController();
|
||||
bool _remember = false;
|
||||
bool _passwordIsWrong = false;
|
||||
bool _isObscure = true;
|
||||
|
||||
void _submit() async {
|
||||
setState(() {
|
||||
_passwordIsWrong = false;
|
||||
});
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget._devicePath).notifier)
|
||||
.unlock(_passwordController.text, remember: _remember);
|
||||
if (!mounted) return;
|
||||
if (!result.first) {
|
||||
setState(() {
|
||||
_passwordIsWrong = true;
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else if (_remember && !result.second) {
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.oath_failed_remember_pw);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keystoreFailed = widget.keystore == KeystoreState.failed;
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 18.0, right: 18, top: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.oath_enter_oath_pw,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
TextField(
|
||||
key: keys.passwordField,
|
||||
controller: _passwordController,
|
||||
autofocus: true,
|
||||
obscureText: _isObscure,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.oath_password,
|
||||
errorText: _passwordIsWrong
|
||||
? AppLocalizations.of(context)!.oath_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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setState(() {
|
||||
_passwordIsWrong = false;
|
||||
}), // Update state on change
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
keystoreFailed
|
||||
? ListTile(
|
||||
leading: const Icon(Icons.warning_amber_rounded),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.oath_keystore_unavailable),
|
||||
dense: true,
|
||||
minLeadingWidth: 0,
|
||||
)
|
||||
: CheckboxListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context)!.oath_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.icon(
|
||||
key: keys.unlockButton,
|
||||
label: Text(AppLocalizations.of(context)!.oath_unlock),
|
||||
icon: const Icon(Icons.lock_open),
|
||||
onPressed: _passwordController.text.isNotEmpty ? _submit : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user