mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Refactor FIDO PIN handling.
This commit is contained in:
parent
6b3bd585ba
commit
37883a1427
@ -46,17 +46,11 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
|||||||
final RpcNodeSession _session;
|
final RpcNodeSession _session;
|
||||||
_DesktopFidoStateNotifier(this._session) : super();
|
_DesktopFidoStateNotifier(this._session) : super();
|
||||||
|
|
||||||
Future<void> refresh() async {
|
Future<void> refresh() => updateState(() async {
|
||||||
try {
|
|
||||||
final result = await _session.command('get');
|
final result = await _session.command('get');
|
||||||
_log.config('application status', jsonEncode(result));
|
_log.config('application status', jsonEncode(result));
|
||||||
final fidoState = FidoState.fromJson(result['data']);
|
return FidoState.fromJson(result['data']);
|
||||||
setState(fidoState);
|
});
|
||||||
} catch (error) {
|
|
||||||
_log.severe('Unable to update FIDO state', jsonEncode(error));
|
|
||||||
setFailure('Failed to update FIDO');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<InteractionEvent> reset() {
|
Stream<InteractionEvent> reset() {
|
||||||
@ -104,6 +98,39 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final desktopFidoPinProvider = StateNotifierProvider.autoDispose
|
||||||
|
.family<PinNotifier, bool, DevicePath>((ref, devicePath) {
|
||||||
|
return _DesktopPinNotifier(ref.watch(_sessionProvider(devicePath)),
|
||||||
|
ref.watch(_pinProvider(devicePath).notifier));
|
||||||
|
});
|
||||||
|
|
||||||
|
class _DesktopPinNotifier extends PinNotifier {
|
||||||
|
final RpcNodeSession _session;
|
||||||
|
final StateController<String?> _pinController;
|
||||||
|
|
||||||
|
_DesktopPinNotifier(this._session, this._pinController)
|
||||||
|
: super(_pinController.state != null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PinResult> unlock(String pin) async {
|
||||||
|
try {
|
||||||
|
await _session.command(
|
||||||
|
'verify_pin',
|
||||||
|
params: {'pin': pin},
|
||||||
|
);
|
||||||
|
_pinController.state = pin;
|
||||||
|
|
||||||
|
return PinResult.success();
|
||||||
|
} on RpcError catch (e) {
|
||||||
|
if (e.status == 'pin-validation') {
|
||||||
|
_pinController.state = null;
|
||||||
|
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
||||||
FidoFingerprintsNotifier,
|
FidoFingerprintsNotifier,
|
||||||
AsyncValue<List<Fingerprint>>,
|
AsyncValue<List<Fingerprint>>,
|
||||||
@ -116,7 +143,7 @@ final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
|||||||
session.setErrorHandler('auth-required', (_) async {
|
session.setErrorHandler('auth-required', (_) async {
|
||||||
final pin = ref.read(_pinProvider(devicePath));
|
final pin = ref.read(_pinProvider(devicePath));
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
await notifier.unlock(pin, remember: false);
|
await notifier._unlock(pin);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
@ -127,40 +154,33 @@ final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
|
|||||||
|
|
||||||
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
|
||||||
final RpcNodeSession _session;
|
final RpcNodeSession _session;
|
||||||
final StateController<String?> _pin;
|
final StateController<String?> _pinNotifier;
|
||||||
bool locked = true;
|
|
||||||
|
|
||||||
_DesktopFidoFingerprintsNotifier(this._session, this._pin) {
|
_DesktopFidoFingerprintsNotifier(this._session, this._pinNotifier) {
|
||||||
final pin = _pin.state;
|
final pin = _pinNotifier.state;
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
unlock(pin, remember: false);
|
_unlock(pin);
|
||||||
} else {
|
} else {
|
||||||
state = const AsyncValue.error('locked');
|
state = const AsyncValue.error('locked');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _unlock(String pin) async {
|
||||||
Future<PinResult> unlock(String pin, {bool remember = true}) async {
|
|
||||||
try {
|
try {
|
||||||
await _session.command(
|
await _session.command(
|
||||||
'unlock',
|
'unlock',
|
||||||
target: ['fingerprints'],
|
target: ['fingerprints'],
|
||||||
params: {'pin': pin},
|
params: {'pin': pin},
|
||||||
);
|
);
|
||||||
locked = false;
|
|
||||||
if (remember) {
|
|
||||||
_pin.state = pin;
|
|
||||||
}
|
|
||||||
await _refresh();
|
await _refresh();
|
||||||
return PinResult.success();
|
|
||||||
} on RpcError catch (e) {
|
} on RpcError catch (e) {
|
||||||
if (e.status == 'pin-validation') {
|
if (e.status == 'pin-validation') {
|
||||||
_pin.state = null;
|
_pinNotifier.state = null;
|
||||||
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
} else {
|
||||||
}
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
final result = await _session.command('fingerprints');
|
final result = await _session.command('fingerprints');
|
||||||
@ -242,7 +262,7 @@ final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
|
|||||||
session.setErrorHandler('auth-required', (_) async {
|
session.setErrorHandler('auth-required', (_) async {
|
||||||
final pin = ref.read(_pinProvider(devicePath));
|
final pin = ref.read(_pinProvider(devicePath));
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
await notifier.unlock(pin, remember: false);
|
await notifier._unlock(pin);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
@ -253,40 +273,33 @@ final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
|
|||||||
|
|
||||||
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
|
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
|
||||||
final RpcNodeSession _session;
|
final RpcNodeSession _session;
|
||||||
final StateController<String?> _pin;
|
final StateController<String?> _pinNotifier;
|
||||||
bool locked = true;
|
|
||||||
|
|
||||||
_DesktopFidoCredentialsNotifier(this._session, this._pin) {
|
_DesktopFidoCredentialsNotifier(this._session, this._pinNotifier) {
|
||||||
final pin = _pin.state;
|
final pin = _pinNotifier.state;
|
||||||
if (pin != null) {
|
if (pin != null) {
|
||||||
unlock(pin, remember: false);
|
_unlock(pin);
|
||||||
} else {
|
} else {
|
||||||
state = const AsyncValue.error('locked');
|
state = const AsyncValue.error('locked');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _unlock(String pin) async {
|
||||||
Future<PinResult> unlock(String pin, {bool remember = true}) async {
|
|
||||||
try {
|
try {
|
||||||
await _session.command(
|
await _session.command(
|
||||||
'unlock',
|
'unlock',
|
||||||
target: ['credentials'],
|
target: ['credentials'],
|
||||||
params: {'pin': pin},
|
params: {'pin': pin},
|
||||||
);
|
);
|
||||||
locked = false;
|
|
||||||
if (remember) {
|
|
||||||
_pin.state = pin;
|
|
||||||
}
|
|
||||||
await _refresh();
|
await _refresh();
|
||||||
return PinResult.success();
|
|
||||||
} on RpcError catch (e) {
|
} on RpcError catch (e) {
|
||||||
if (e.status == 'pin-validation') {
|
if (e.status == 'pin-validation') {
|
||||||
_pin.state = null;
|
_pinNotifier.state = null;
|
||||||
return PinResult.failed(e.body['retries'], e.body['auth_blocked']);
|
} else {
|
||||||
}
|
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _refresh() async {
|
Future<void> _refresh() async {
|
||||||
final List<FidoCredential> creds = [];
|
final List<FidoCredential> creds = [];
|
||||||
|
@ -100,6 +100,7 @@ Future<List<Override>> initializeAndGetOverrides(
|
|||||||
qrScannerProvider.overrideWithProvider(desktopQrScannerProvider),
|
qrScannerProvider.overrideWithProvider(desktopQrScannerProvider),
|
||||||
managementStateProvider.overrideWithProvider(desktopManagementState),
|
managementStateProvider.overrideWithProvider(desktopManagementState),
|
||||||
fidoStateProvider.overrideWithProvider(desktopFidoState),
|
fidoStateProvider.overrideWithProvider(desktopFidoState),
|
||||||
|
fidoPinProvider.overrideWithProvider(desktopFidoPinProvider),
|
||||||
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
|
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
|
||||||
credentialProvider.overrideWithProvider(desktopCredentialProvider),
|
credentialProvider.overrideWithProvider(desktopCredentialProvider),
|
||||||
currentDeviceProvider.overrideWithProvider(desktopCurrentDeviceProvider)
|
currentDeviceProvider.overrideWithProvider(desktopCurrentDeviceProvider)
|
||||||
|
@ -15,10 +15,19 @@ abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
|
|||||||
Future<PinResult> setPin(String newPin, {String? oldPin});
|
Future<PinResult> setPin(String newPin, {String? oldPin});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final fidoPinProvider =
|
||||||
|
StateNotifierProvider.autoDispose.family<PinNotifier, bool, DevicePath>(
|
||||||
|
(ref, devicePath) => throw UnimplementedError(),
|
||||||
|
);
|
||||||
|
|
||||||
|
abstract class PinNotifier extends StateNotifier<bool> {
|
||||||
|
PinNotifier(bool unlocked) : super(unlocked);
|
||||||
|
Future<PinResult> unlock(String pin);
|
||||||
|
}
|
||||||
|
|
||||||
abstract class LockedCollectionNotifier<T>
|
abstract class LockedCollectionNotifier<T>
|
||||||
extends StateNotifier<AsyncValue<List<T>>> {
|
extends StateNotifier<AsyncValue<List<T>>> {
|
||||||
LockedCollectionNotifier() : super(const AsyncValue.loading());
|
LockedCollectionNotifier() : super(const AsyncValue.loading());
|
||||||
Future<PinResult> unlock(String pin);
|
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void setItems(List<T> items) {
|
void setItems(List<T> items) {
|
||||||
|
@ -41,12 +41,15 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Animation<Color?> _animateColor(Color color, {Function? atPeak}) {
|
Animation<Color?> _animateColor(Color color,
|
||||||
|
{Function? atPeak, bool reverse = true}) {
|
||||||
final animation =
|
final animation =
|
||||||
ColorTween(begin: Colors.black, end: color).animate(_animator);
|
ColorTween(begin: Colors.black, end: color).animate(_animator);
|
||||||
_animator.forward().then((_) {
|
_animator.forward().then((_) {
|
||||||
|
if (reverse) {
|
||||||
atPeak?.call();
|
atPeak?.call();
|
||||||
_animator.reverse();
|
_animator.reverse();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return animation;
|
return animation;
|
||||||
}
|
}
|
||||||
@ -72,7 +75,7 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
|
|||||||
_samples += 1;
|
_samples += 1;
|
||||||
_remaining = remaining;
|
_remaining = remaining;
|
||||||
});
|
});
|
||||||
});
|
}, reverse: remaining > 0);
|
||||||
}, complete: (fingerprint) {
|
}, complete: (fingerprint) {
|
||||||
_remaining = 0;
|
_remaining = 0;
|
||||||
_fingerprint = fingerprint;
|
_fingerprint = fingerprint;
|
||||||
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../app/views/app_failure_screen.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'delete_credential_dialog.dart';
|
import 'delete_credential_dialog.dart';
|
||||||
import 'rename_credential_dialog.dart';
|
import 'rename_credential_dialog.dart';
|
||||||
import 'unlock_view.dart';
|
|
||||||
|
|
||||||
class CredentialPage extends ConsumerWidget {
|
class CredentialPage extends ConsumerWidget {
|
||||||
final DeviceNode node;
|
final DeviceNode node;
|
||||||
@ -18,13 +18,7 @@ class CredentialPage extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ref.watch(credentialProvider(node.path)).when(
|
return ref.watch(credentialProvider(node.path)).when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (error, _) => UnlockView(
|
error: (error, _) => AppFailureScreen('$error'),
|
||||||
onUnlock: (pin) async {
|
|
||||||
return ref
|
|
||||||
.read(credentialProvider(node.path).notifier)
|
|
||||||
.unlock(pin);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
data: (credentials) => ListView(
|
data: (credentials) => ListView(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../app/views/app_failure_screen.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'add_fingerprint_dialog.dart';
|
import 'add_fingerprint_dialog.dart';
|
||||||
import 'delete_fingerprint_dialog.dart';
|
import 'delete_fingerprint_dialog.dart';
|
||||||
import 'rename_fingerprint_dialog.dart';
|
import 'rename_fingerprint_dialog.dart';
|
||||||
import 'unlock_view.dart';
|
|
||||||
|
|
||||||
class FingerprintPage extends ConsumerWidget {
|
class FingerprintPage extends ConsumerWidget {
|
||||||
final DeviceNode node;
|
final DeviceNode node;
|
||||||
@ -19,13 +19,7 @@ class FingerprintPage extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ref.watch(fingerprintProvider(node.path)).when(
|
return ref.watch(fingerprintProvider(node.path)).when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (error, _) => UnlockView(
|
error: (error, _) => AppFailureScreen('$error'),
|
||||||
onUnlock: (pin) async {
|
|
||||||
return ref
|
|
||||||
.read(fingerprintProvider(node.path).notifier)
|
|
||||||
.unlock(pin);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
data: (fingerprints) => ListView(
|
data: (fingerprints) => ListView(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
import '../state.dart';
|
||||||
import 'pin_dialog.dart';
|
import 'pin_dialog.dart';
|
||||||
|
import 'pin_entry_dialog.dart';
|
||||||
import 'reset_dialog.dart';
|
import 'reset_dialog.dart';
|
||||||
|
|
||||||
class FidoMainPage extends StatelessWidget {
|
class FidoMainPage extends ConsumerWidget {
|
||||||
final DeviceNode node;
|
final DeviceNode node;
|
||||||
final FidoState state;
|
final FidoState state;
|
||||||
final Function(SubPage page) setSubPage;
|
final Function(SubPage page) setSubPage;
|
||||||
@ -14,8 +17,21 @@ class FidoMainPage extends StatelessWidget {
|
|||||||
{required this.setSubPage, Key? key})
|
{required this.setSubPage, Key? key})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
|
_openLockedPage(BuildContext context, WidgetRef ref, SubPage subPage) async {
|
||||||
|
final unlocked = ref.read(fidoPinProvider(node.path));
|
||||||
|
if (unlocked) {
|
||||||
|
setSubPage(subPage);
|
||||||
|
} else {
|
||||||
|
final result = await showDialog(
|
||||||
|
context: context, builder: (context) => PinEntryDialog(node.path));
|
||||||
|
if (result == true) {
|
||||||
|
setSubPage(subPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -41,7 +57,7 @@ class FidoMainPage extends StatelessWidget {
|
|||||||
? 'Fingerprints have been registered'
|
? 'Fingerprints have been registered'
|
||||||
: 'No fingerprints registered'),
|
: 'No fingerprints registered'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setSubPage(SubPage.fingerprints);
|
_openLockedPage(context, ref, SubPage.fingerprints);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (state.credMgmt)
|
if (state.credMgmt)
|
||||||
@ -50,14 +66,13 @@ class FidoMainPage extends StatelessWidget {
|
|||||||
child: Icon(Icons.account_box),
|
child: Icon(Icons.account_box),
|
||||||
),
|
),
|
||||||
title: const Text('Credentials'),
|
title: const Text('Credentials'),
|
||||||
|
enabled: state.hasPin,
|
||||||
subtitle: Text(state.hasPin
|
subtitle: Text(state.hasPin
|
||||||
? 'Manage stored credentials on key'
|
? 'Manage stored credentials on key'
|
||||||
: 'Set a PIN to manage credentials'),
|
: 'Set a PIN to manage credentials'),
|
||||||
onTap: state.hasPin
|
onTap: () {
|
||||||
? () {
|
_openLockedPage(context, ref, SubPage.credentials);
|
||||||
setSubPage(SubPage.credentials);
|
},
|
||||||
}
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const CircleAvatar(
|
leading: const CircleAvatar(
|
||||||
|
93
lib/fido/views/pin_entry_dialog.dart
Executable file
93
lib/fido/views/pin_entry_dialog.dart
Executable file
@ -0,0 +1,93 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../app/models.dart';
|
||||||
|
import '../../app/state.dart';
|
||||||
|
import '../state.dart';
|
||||||
|
|
||||||
|
class PinEntryDialog extends ConsumerStatefulWidget {
|
||||||
|
final DevicePath _devicePath;
|
||||||
|
const PinEntryDialog(this._devicePath, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PinEntryDialog> createState() => _PinEntryDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PinEntryDialogState extends ConsumerState<PinEntryDialog> {
|
||||||
|
final _pinController = TextEditingController();
|
||||||
|
bool _blocked = false;
|
||||||
|
int? _retries;
|
||||||
|
|
||||||
|
void _submit() async {
|
||||||
|
final result = await ref
|
||||||
|
.read(fidoPinProvider(widget._devicePath).notifier)
|
||||||
|
.unlock(_pinController.text);
|
||||||
|
result.when(success: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}, failed: (retries, authBlocked) {
|
||||||
|
setState(() {
|
||||||
|
_pinController.clear();
|
||||||
|
_retries = retries;
|
||||||
|
_blocked = authBlocked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getErrorText() {
|
||||||
|
if (_retries == 0) {
|
||||||
|
return 'PIN is blocked. Factory reset the FIDO application.';
|
||||||
|
}
|
||||||
|
if (_blocked) {
|
||||||
|
return 'PIN temporarily blocked, remove and reinsert your YubiKey.';
|
||||||
|
}
|
||||||
|
if (_retries != null) {
|
||||||
|
return 'Wrong PIN. $_retries attempts remaining.';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// If current device changes, we need to pop back to the main Page.
|
||||||
|
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
});
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: const Text('Enter PIN'),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Enter the FIDO PIN for your YubiKey.',
|
||||||
|
),
|
||||||
|
TextField(
|
||||||
|
autofocus: true,
|
||||||
|
obscureText: true,
|
||||||
|
controller: _pinController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'PIN',
|
||||||
|
errorText: _getErrorText(),
|
||||||
|
),
|
||||||
|
onChanged: (_) => setState(() {}), // Update state on change
|
||||||
|
onSubmitted: (_) => _submit(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Continue'),
|
||||||
|
onPressed:
|
||||||
|
_pinController.text.isNotEmpty && !_blocked ? _submit : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,44 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../models.dart';
|
|
||||||
|
|
||||||
class UnlockView extends StatelessWidget {
|
|
||||||
final Future<PinResult> Function(String pin) onUnlock;
|
|
||||||
|
|
||||||
const UnlockView({required this.onUnlock, Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
|
||||||
child: Text(
|
|
||||||
'Enter PIN',
|
|
||||||
style: Theme.of(context).textTheme.headline5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'Enter the FIDO PIN for your YubiKey. If you don\'t know your PIN, you\'ll need to reset the YubiKey.',
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
autofocus: true,
|
|
||||||
obscureText: true,
|
|
||||||
decoration: const InputDecoration(labelText: 'PIN'),
|
|
||||||
onSubmitted: (pin) async {
|
|
||||||
// TODO: Handle wrong PIN
|
|
||||||
await onUnlock(pin);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user