diff --git a/lib/app/models.dart b/lib/app/models.dart index 2dc27a69..96ceaced 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -65,6 +65,18 @@ enum Application { _ => name.substring(0, 1).toUpperCase() + name.substring(1), }; + Capability? getCapability() => switch (this) { + Application.accounts => Capability.oath, + Application.webauthn => Capability.u2f, + Application.passkeys => Capability.fido2, + Application.fingerprints => Capability.fido2, + Application.certificates => Capability.piv, + Application.slots => Capability.otp, + Application.hsmauth => Capability.hsmauth, + Application.openpgp => Capability.openpgp, + _ => null + }; + Availability getAvailability(YubiKeyData data) { if (this == Application.management) { final version = data.info.version; diff --git a/lib/app/views/app_page.dart b/lib/app/views/app_page.dart index 5175e3d7..1c352dd0 100755 --- a/lib/app/views/app_page.dart +++ b/lib/app/views/app_page.dart @@ -44,7 +44,7 @@ class AppPage extends StatelessWidget { final Widget Function(BuildContext context)? actionButtonBuilder; final Widget? fileDropOverlay; final Function(File file)? onFileDropped; - final List? capabilities; + final Capability? capability; const AppPage({ super.key, this.title, @@ -55,7 +55,7 @@ class AppPage extends StatelessWidget { this.detailViewBuilder, this.actionButtonBuilder, this.fileDropOverlay, - this.capabilities, + this.capability, this.onFileDropped, this.delayedContent = false, this.keyActionsBadge = false, @@ -169,12 +169,7 @@ class AppPage extends StatelessWidget { .colorScheme .primary .withOpacity(0.9))), - if (capabilities != null) - Wrap( - spacing: 4.0, - runSpacing: 8.0, - children: [...capabilities!.map((c) => _CapabilityBadge(c))], - ) + if (capability != null) _CapabilityBadge(capability!) ]) ], ); @@ -182,7 +177,8 @@ class AppPage extends StatelessWidget { Widget _buildMainContent(BuildContext context, bool expanded) { final content = Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + centered ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ if (title != null) Padding( diff --git a/lib/app/views/device_error_screen.dart b/lib/app/views/device_error_screen.dart index aa88fe6a..316bf96c 100755 --- a/lib/app/views/device_error_screen.dart +++ b/lib/app/views/device_error_screen.dart @@ -25,7 +25,6 @@ import '../../desktop/state.dart'; import '../message.dart'; import '../models.dart'; import '../state.dart'; -import 'device_avatar.dart'; import 'message_page.dart'; class DeviceErrorScreen extends ConsumerWidget { @@ -72,8 +71,13 @@ class DeviceErrorScreen extends ConsumerWidget { } return MessagePage( centered: true, - graphic: const DeviceAvatar(child: Icon(Icons.usb_off)), - message: l10n.l_yk_no_access, + graphic: Image.asset( + 'assets/product-images/generic.png', + filterQuality: FilterQuality.medium, + scale: 3, + color: Theme.of(context).colorScheme.error, + ), + header: l10n.l_yk_no_access, ); } @@ -86,11 +90,11 @@ class DeviceErrorScreen extends ConsumerWidget { 'unknown-device' => MessagePage( centered: true, graphic: Icon( - Icons.help_outline, + Icons.help_outlined, size: 96, - color: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.error, ), - message: l10n.s_unknown_device, + header: l10n.s_unknown_device, ), _ => MessagePage( centered: true, diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index 463ae55b..fe2e6fbe 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -26,10 +26,12 @@ import '../../exception/cancellation_exception.dart'; import '../../fido/views/fingerprints_screen.dart'; import '../../fido/views/passkeys_screen.dart'; import '../../fido/views/webauthn_page.dart'; +import '../../management/views/management_screen.dart'; import '../../oath/views/oath_screen.dart'; import '../../otp/views/otp_screen.dart'; import '../../piv/views/piv_screen.dart'; import '../../widgets/custom_icons.dart'; +import '../message.dart'; import '../models.dart'; import '../state.dart'; import 'device_error_screen.dart'; @@ -131,21 +133,48 @@ class MainPage extends ConsumerWidget { return ref.watch(currentDeviceDataProvider).when( data: (data) { final app = ref.watch(currentAppProvider); + final capability = app.getCapability(); if (data.info.supportedCapabilities.isEmpty && data.name == 'Unrecognized device') { return MessagePage( + centered: true, + graphic: Icon( + Icons.help_outlined, + size: 96, + color: Theme.of(context).colorScheme.error, + ), header: l10n.s_yk_not_recognized, ); } else if (app.getAvailability(data) == Availability.unsupported) { return MessagePage( + title: app.getDisplayName(l10n), + capability: capability, header: l10n.s_app_not_supported, - message: l10n.l_app_not_supported_on_yk(app.name), + message: l10n.l_app_not_supported_on_yk( + capability?.getDisplayName(l10n) ?? app.name), ); } else if (app.getAvailability(data) != Availability.enabled) { return MessagePage( + title: app.getDisplayName(l10n), + capability: capability, header: l10n.s_app_disabled, - message: l10n.l_app_disabled_desc(app.name), + message: l10n.l_app_disabled_desc( + capability?.getDisplayName(l10n) ?? app.name), + actions: [ + ActionChip( + label: Text(data.info.version.major > 4 + ? l10n.s_toggle_applications + : l10n.s_toggle_interfaces), + onPressed: () async { + await showBlurDialog( + context: context, + builder: (context) => ManagementScreen(data), + ); + }, + avatar: const Icon(Icons.construction), + ) + ], ); } diff --git a/lib/app/views/message_page.dart b/lib/app/views/message_page.dart index 996c64e4..3c5e373b 100755 --- a/lib/app/views/message_page.dart +++ b/lib/app/views/message_page.dart @@ -32,9 +32,9 @@ class MessagePage extends StatelessWidget { final Widget Function(BuildContext context)? actionButtonBuilder; final Widget? fileDropOverlay; final Function(File file)? onFileDropped; - final List? capabilities; + final Capability? capability; final bool keyActionsBadge; - final bool? centered; + final bool centered; const MessagePage({ super.key, @@ -49,15 +49,15 @@ class MessagePage extends StatelessWidget { this.onFileDropped, this.delayedContent = false, this.keyActionsBadge = false, - this.capabilities, - this.centered, + this.capability, + this.centered = false, }); @override Widget build(BuildContext context) => AppPage( title: title, - capabilities: capabilities, - centered: centered ?? false, + capability: capability, + centered: centered, actions: actions, keyActionsBuilder: keyActionsBuilder, keyActionsBadge: keyActionsBadge, @@ -70,11 +70,11 @@ class MessagePage extends StatelessWidget { left: 16.0, top: 0.0, right: 16.0, - bottom: centered ?? false ? 96 : 0), + bottom: centered && actions.isEmpty ? 96 : 0), child: SizedBox( width: 350, child: Column( - crossAxisAlignment: centered ?? false + crossAxisAlignment: centered ? CrossAxisAlignment.center : CrossAxisAlignment.start, children: [ @@ -84,17 +84,14 @@ class MessagePage extends StatelessWidget { ], if (header != null) Text(header!, - textAlign: - centered ?? false ? TextAlign.center : TextAlign.left, + textAlign: centered ? TextAlign.center : TextAlign.left, style: Theme.of(context).textTheme.titleLarge), if (message != null) ...[ const SizedBox(height: 12.0), Container( constraints: const BoxConstraints(maxWidth: 300), child: Text(message!, - textAlign: centered ?? false - ? TextAlign.center - : TextAlign.left, + textAlign: centered ? TextAlign.center : TextAlign.left, style: Theme.of(context).textTheme.titleSmall?.apply( color: Theme.of(context) .colorScheme diff --git a/lib/fido/views/fingerprints_screen.dart b/lib/fido/views/fingerprints_screen.dart index 09e7fdc3..681a36ef 100644 --- a/lib/fido/views/fingerprints_screen.dart +++ b/lib/fido/views/fingerprints_screen.dart @@ -61,7 +61,7 @@ class FingerprintsScreen extends ConsumerWidget { if (Capability.fido2.value & enabled == 0) { return MessagePage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: l10n.s_fido_disabled, message: l10n.l_webauthn_req_fido2, ); @@ -105,7 +105,7 @@ class _FidoLockedPage extends ConsumerWidget { ) ], title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: '${l10n.s_fingerprints_get_started} (1/2)', message: l10n.p_set_fingerprints_desc, keyActionsBuilder: hasActions ? _buildActions : null, @@ -116,17 +116,28 @@ class _FidoLockedPage extends ConsumerWidget { if (state.forcePinChange) { return MessagePage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: l10n.s_pin_change_required, message: l10n.l_pin_change_required_desc, keyActionsBuilder: hasActions ? _buildActions : null, keyActionsBadge: fidoShowActionsNotifier(state), + actions: [ + ActionChip( + label: Text(l10n.s_change_pin), + onPressed: () async { + await showBlurDialog( + context: context, + builder: (context) => FidoPinDialog(node.path, state)); + }, + avatar: const Icon(Icons.pin_outlined), + ) + ], ); } return AppPage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capability: Capability.fido2, keyActionsBuilder: hasActions ? _buildActions : null, builder: (context, _) => Column( children: [ @@ -179,7 +190,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { ) ], title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: '${l10n.s_fingerprints_get_started} (2/2)', message: l10n.l_add_one_or_more_fps, keyActionsBuilder: hasActions @@ -239,7 +250,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { }, builder: (context) => AppPage( title: l10n.s_fingerprints, - capabilities: const [Capability.fido2], + capability: Capability.fido2, detailViewBuilder: fingerprint != null ? (context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/fido/views/passkeys_screen.dart b/lib/fido/views/passkeys_screen.dart index 8ab04ffb..b9e2e50b 100644 --- a/lib/fido/views/passkeys_screen.dart +++ b/lib/fido/views/passkeys_screen.dart @@ -37,6 +37,7 @@ import '../state.dart'; import 'actions.dart'; import 'credential_dialog.dart'; import 'key_actions.dart'; +import 'pin_dialog.dart'; import 'pin_entry_form.dart'; class PasskeysScreen extends ConsumerWidget { @@ -56,10 +57,11 @@ class PasskeysScreen extends ConsumerWidget { final enabled = deviceData .info.config.enabledCapabilities[deviceData.node.transport] ?? 0; + if (Capability.fido2.value & enabled == 0) { return MessagePage( title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: l10n.s_fido_disabled, message: l10n.l_webauthn_req_fido2, ); @@ -92,7 +94,7 @@ class _FidoLockedPage extends ConsumerWidget { if (!state.hasPin) { return MessagePage( title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: state.credMgmt ? l10n.l_no_discoverable_accounts : l10n.l_ready_to_use, @@ -105,7 +107,7 @@ class _FidoLockedPage extends ConsumerWidget { if (!state.credMgmt && state.bioEnroll == null) { return MessagePage( title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: l10n.l_ready_to_use, message: l10n.l_register_sk_on_websites, keyActionsBuilder: hasActions ? _buildActions : null, @@ -115,8 +117,19 @@ class _FidoLockedPage extends ConsumerWidget { if (state.forcePinChange) { return MessagePage( + actions: [ + ActionChip( + label: Text(l10n.s_change_pin), + onPressed: () async { + await showBlurDialog( + context: context, + builder: (context) => FidoPinDialog(node.path, state)); + }, + avatar: const Icon(Icons.pin_outlined), + ) + ], title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: l10n.s_pin_change_required, message: l10n.l_pin_change_required_desc, keyActionsBuilder: hasActions ? _buildActions : null, @@ -126,7 +139,7 @@ class _FidoLockedPage extends ConsumerWidget { return AppPage( title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, keyActionsBuilder: hasActions ? _buildActions : null, builder: (context, _) => Column( children: [ @@ -164,7 +177,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { // TODO: Special handling for credMgmt not supported return MessagePage( title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: l10n.l_no_discoverable_accounts, message: l10n.l_register_sk_on_websites, keyActionsBuilder: hasActions @@ -184,7 +197,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { if (credentials.isEmpty) { return MessagePage( title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, header: l10n.l_no_discoverable_accounts, message: l10n.l_register_sk_on_websites, keyActionsBuilder: hasActions @@ -233,7 +246,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { }, builder: (context) => AppPage( title: l10n.s_passkeys, - capabilities: const [Capability.fido2], + capability: Capability.fido2, detailViewBuilder: credential != null ? (context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/fido/views/webauthn_page.dart b/lib/fido/views/webauthn_page.dart index 2efbc1b0..79263c9f 100644 --- a/lib/fido/views/webauthn_page.dart +++ b/lib/fido/views/webauthn_page.dart @@ -28,7 +28,7 @@ class WebAuthnScreen extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return MessagePage( title: l10n.s_webauthn, - capabilities: const [Capability.u2f], + capability: Capability.u2f, header: l10n.l_ready_to_use, message: l10n.l_register_sk_on_websites, ); diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index b82e53a5..11ac356f 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -83,7 +83,7 @@ class _LockedView extends ConsumerWidget { final hasActions = ref.watch(featureProvider)(features.actions); return AppPage( title: AppLocalizations.of(context)!.s_accounts, - capabilities: const [Capability.oath], + capability: Capability.oath, keyActionsBuilder: hasActions ? (context) => oathBuildActions(context, devicePath, oathState, ref) : null, @@ -175,7 +175,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { ) ], title: l10n.s_accounts, - capabilities: const [Capability.oath], + capability: Capability.oath, key: keys.noAccountsView, header: l10n.l_authenticator_get_started, message: l10n.p_no_accounts_desc, @@ -249,7 +249,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { }, builder: (context) => AppPage( title: l10n.s_accounts, - capabilities: const [Capability.oath], + capability: Capability.oath, keyActionsBuilder: hasActions ? (context) => oathBuildActions( context, diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index a813e03c..6107ea49 100644 --- a/lib/otp/views/otp_screen.dart +++ b/lib/otp/views/otp_screen.dart @@ -93,7 +93,7 @@ class _OtpScreenState extends ConsumerState { }, child: AppPage( title: l10n.s_slots, - capabilities: const [Capability.otp], + capability: Capability.otp, detailViewBuilder: selected != null ? (context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/piv/views/piv_screen.dart b/lib/piv/views/piv_screen.dart index c5db15a1..14cf4524 100644 --- a/lib/piv/views/piv_screen.dart +++ b/lib/piv/views/piv_screen.dart @@ -106,7 +106,7 @@ class _PivScreenState extends ConsumerState { }, child: AppPage( title: l10n.s_certificates, - capabilities: const [Capability.piv], + capability: Capability.piv, detailViewBuilder: selected != null ? (context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch,