From 9eeb44f3acc503fbc5d134509dbdf180f13786c4 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Thu, 4 May 2023 16:20:50 +0200 Subject: [PATCH] Update FIDO passkeys views. --- lib/fido/views/credential_dialog.dart | 114 +++++++++++++++++++ lib/fido/views/delete_credential_dialog.dart | 8 +- lib/fido/views/unlocked_page.dart | 83 ++++++++------ lib/l10n/app_en.arb | 15 +-- 4 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 lib/fido/views/credential_dialog.dart diff --git a/lib/fido/views/credential_dialog.dart b/lib/fido/views/credential_dialog.dart new file mode 100644 index 00000000..db9b78ea --- /dev/null +++ b/lib/fido/views/credential_dialog.dart @@ -0,0 +1,114 @@ +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/shortcuts.dart'; +import '../../app/state.dart'; +import '../../app/views/fs_dialog.dart'; +import '../../widgets/list_title.dart'; +import '../models.dart'; +import 'delete_credential_dialog.dart'; + +class CredentialDialog extends ConsumerWidget { + final FidoCredential credential; + const CredentialDialog(this.credential, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // TODO: Solve this in a cleaner way + final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node; + if (node == null) { + // The rest of this method assumes there is a device, and will throw an exception if not. + // This will never be shown, as the dialog will be immediately closed + return const SizedBox(); + } + + return Actions( + actions: { + DeleteIntent: CallbackAction(onInvoke: (_) async { + final withContext = ref.read(withContextProvider); + final bool? deleted = + await ref.read(withContextProvider)((context) async => + await showBlurDialog( + context: context, + builder: (context) => DeleteCredentialDialog( + node.path, + credential, + ), + ) ?? + false); + + // Pop the account dialog if deleted + if (deleted == true) { + await withContext((context) async { + Navigator.of(context).pop(); + }); + } + return deleted; + }), + }, + child: FocusScope( + autofocus: true, + child: FsDialog( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 48, bottom: 32), + child: Column( + children: [ + Text( + credential.userName, + style: Theme.of(context).textTheme.headlineSmall, + softWrap: true, + textAlign: TextAlign.center, + ), + Text( + credential.rpId, + softWrap: true, + textAlign: TextAlign.center, + // This is what ListTile uses for subtitle + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).textTheme.bodySmall!.color, + ), + ), + const SizedBox(height: 16), + const Icon(Icons.person, size: 72), + ], + ), + ), + ListTitle(AppLocalizations.of(context)!.s_actions, + textStyle: Theme.of(context).textTheme.bodyLarge), + _CredentialDialogActions(), + ], + ), + ), + ), + ); + } +} + +class _CredentialDialogActions extends StatelessWidget { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = + ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; + return Column( + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: theme.error, + foregroundColor: theme.onError, + child: const Icon(Icons.delete), + ), + title: Text(l10n.s_delete_passkey), + subtitle: Text(l10n.l_delete_account_desc), + onTap: () { + Actions.invoke(context, const DeleteIntent()); + }, + ), + ], + ); + } +} diff --git a/lib/fido/views/delete_credential_dialog.dart b/lib/fido/views/delete_credential_dialog.dart index b1bb30aa..b74279e4 100755 --- a/lib/fido/views/delete_credential_dialog.dart +++ b/lib/fido/views/delete_credential_dialog.dart @@ -38,14 +38,14 @@ class DeleteCredentialDialog extends ConsumerWidget { final label = credential.userName; return ResponsiveDialog( - title: Text(l10n.s_delete_credential), + title: Text(l10n.s_delete_passkey), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(l10n.p_warning_delete_credential), - Text(l10n.l_credential(label)), + Text(l10n.p_warning_delete_passkey), + Text(l10n.l_passkey(label)), ] .map((e) => Padding( child: e, @@ -63,7 +63,7 @@ class DeleteCredentialDialog extends ConsumerWidget { await ref.read(withContextProvider)( (context) async { Navigator.of(context).pop(true); - showMessage(context, l10n.s_credential_deleted); + showMessage(context, l10n.s_passkey_deleted); }, ); }, diff --git a/lib/fido/views/unlocked_page.dart b/lib/fido/views/unlocked_page.dart index 65fe853a..f2f2305f 100755 --- a/lib/fido/views/unlocked_page.dart +++ b/lib/fido/views/unlocked_page.dart @@ -27,7 +27,7 @@ import '../../app/views/message_page.dart'; import '../../widgets/list_title.dart'; import '../models.dart'; import '../state.dart'; -import 'delete_credential_dialog.dart'; +import 'credential_dialog.dart'; import 'fingerprint_dialog.dart'; import 'key_actions.dart'; @@ -48,42 +48,19 @@ class FidoUnlockedPage extends ConsumerWidget { } final creds = data.value; if (creds.isNotEmpty) { - children.add(ListTitle(l10n.s_credentials)); - children.addAll( - creds.map( - (cred) => ListTile( - leading: CircleAvatar( - foregroundColor: Theme.of(context).colorScheme.onPrimary, - backgroundColor: Theme.of(context).colorScheme.primary, - child: const Icon(Icons.person), - ), - title: Text( - cred.userName, - softWrap: false, - overflow: TextOverflow.fade, - ), - subtitle: Text( - cred.rpId, - softWrap: false, - overflow: TextOverflow.fade, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () { - showBlurDialog( - context: context, - builder: (context) => - DeleteCredentialDialog(node.path, cred), - ); - }, - icon: const Icon(Icons.delete_outline)), - ], - ), - ), - ), - ); + children.add(ListTitle(l10n.s_passkeys)); + children.addAll(creds.map((cred) => Actions( + actions: { + OpenIntent: CallbackAction(onInvoke: (_) async { + await showBlurDialog( + context: context, + builder: (context) => CredentialDialog(cred), + ); + return null; + }), + }, + child: _CredentialListItem(cred), + ))); } } @@ -153,6 +130,38 @@ class FidoUnlockedPage extends ConsumerWidget { ); } +class _CredentialListItem extends StatelessWidget { + final FidoCredential credential; + const _CredentialListItem(this.credential); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: CircleAvatar( + foregroundColor: Theme.of(context).colorScheme.onPrimary, + backgroundColor: Theme.of(context).colorScheme.primary, + child: const Icon(Icons.person), + ), + title: Text( + credential.userName, + softWrap: false, + overflow: TextOverflow.fade, + ), + subtitle: Text( + credential.rpId, + softWrap: false, + overflow: TextOverflow.fade, + ), + trailing: OutlinedButton( + onPressed: () { + Actions.maybeInvoke(context, const OpenIntent()); + }, + child: const Icon(Icons.more_horiz), + ), + ); + } +} + class _FingerprintListItem extends StatelessWidget { final Fingerprint fingerprint; const _FingerprintListItem(this.fingerprint); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2103cbcb..3ea7697c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -291,19 +291,20 @@ "l_calculate_code_desc": "Get a new code from your YubiKey", "@_fido_credentials": {}, - "l_credential": "Credential: {label}", - "@l_credential" : { + "l_passkey": "Passkey: {label}", + "@l_passkey" : { "placeholders": { "label": {} } }, - "s_credentials": "Credentials", + "s_passkeys": "Passkeys", "l_ready_to_use": "Ready to use", "l_register_sk_on_websites": "Register as a Security Key on websites", - "l_no_discoverable_accounts": "No discoverable accounts", - "s_delete_credential": "Delete credential", - "s_credential_deleted": "Credential deleted", - "p_warning_delete_credential": "This will delete the credential from your YubiKey.", + "l_no_discoverable_accounts": "No Passkeys stored", + "s_delete_passkey": "Delete Passkey", + "l_delete_passkey_desc": "Remove the Passkey from the YubiKey", + "s_passkey_deleted": "Passkey deleted", + "p_warning_delete_passkey": "This will delete the Passkey from your YubiKey.", "@_fingerprints": {}, "l_fingerprint": "Fingerprint: {label}",