diff --git a/lib/app/models.dart b/lib/app/models.dart index 96ceaced..546b6992 100755 --- a/lib/app/models.dart +++ b/lib/app/models.dart @@ -30,17 +30,23 @@ const _listEquality = ListEquality(); enum Availability { enabled, disabled, unsupported } enum Application { - accounts, - webauthn, - fingerprints, - passkeys, - slots, - certificates, - openpgp, - hsmauth, - management; + accounts([Capability.oath]), + webauthn([Capability.u2f]), + fingerprints([Capability.fido2]), + passkeys([Capability.fido2]), + slots([Capability.otp]), + certificates([Capability.piv]), + openpgp([Capability.openpgp]), + hsmauth([Capability.hsmauth]), + management(); - const Application(); + final List capabilities; + + List getCapabilities() { + return capabilities; + } + + const Application([this.capabilities = const []]); bool _inCapabilities(int capabilities) => switch (this) { Application.accounts => Capability.oath.value & capabilities != 0, @@ -65,18 +71,6 @@ 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 1c352dd0..38df0daf 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 Capability? capability; + final List? capabilities; const AppPage({ super.key, this.title, @@ -55,7 +55,7 @@ class AppPage extends StatelessWidget { this.detailViewBuilder, this.actionButtonBuilder, this.fileDropOverlay, - this.capability, + this.capabilities, this.onFileDropped, this.delayedContent = false, this.keyActionsBadge = false, @@ -169,7 +169,12 @@ class AppPage extends StatelessWidget { .colorScheme .primary .withOpacity(0.9))), - if (capability != null) _CapabilityBadge(capability!) + if (capabilities != null) + Wrap( + spacing: 4.0, + runSpacing: 8.0, + children: [...capabilities!.map((c) => _CapabilityBadge(c))], + ) ]) ], ); diff --git a/lib/app/views/main_page.dart b/lib/app/views/main_page.dart index fe2e6fbe..5d533df9 100755 --- a/lib/app/views/main_page.dart +++ b/lib/app/views/main_page.dart @@ -133,7 +133,7 @@ class MainPage extends ConsumerWidget { return ref.watch(currentDeviceDataProvider).when( data: (data) { final app = ref.watch(currentAppProvider); - final capability = app.getCapability(); + final capabilities = app.getCapabilities(); if (data.info.supportedCapabilities.isEmpty && data.name == 'Unrecognized device') { return MessagePage( @@ -149,18 +149,20 @@ class MainPage extends ConsumerWidget { Availability.unsupported) { return MessagePage( title: app.getDisplayName(l10n), - capability: capability, + capabilities: capabilities, header: l10n.s_app_not_supported, - message: l10n.l_app_not_supported_on_yk( - capability?.getDisplayName(l10n) ?? app.name), + message: l10n.l_app_not_supported_on_yk(capabilities + .map((c) => c.getDisplayName(l10n)) + .join(',')), ); } else if (app.getAvailability(data) != Availability.enabled) { return MessagePage( title: app.getDisplayName(l10n), - capability: capability, + capabilities: capabilities, header: l10n.s_app_disabled, - message: l10n.l_app_disabled_desc( - capability?.getDisplayName(l10n) ?? app.name), + message: l10n.l_app_disabled_desc(capabilities + .map((c) => c.getDisplayName(l10n)) + .join(',')), actions: [ ActionChip( label: Text(data.info.version.major > 4 diff --git a/lib/app/views/message_page.dart b/lib/app/views/message_page.dart index 3c5e373b..dd726929 100755 --- a/lib/app/views/message_page.dart +++ b/lib/app/views/message_page.dart @@ -32,7 +32,7 @@ class MessagePage extends StatelessWidget { final Widget Function(BuildContext context)? actionButtonBuilder; final Widget? fileDropOverlay; final Function(File file)? onFileDropped; - final Capability? capability; + final List? capabilities; final bool keyActionsBadge; final bool centered; @@ -49,14 +49,14 @@ class MessagePage extends StatelessWidget { this.onFileDropped, this.delayedContent = false, this.keyActionsBadge = false, - this.capability, + this.capabilities, this.centered = false, }); @override Widget build(BuildContext context) => AppPage( title: title, - capability: capability, + capabilities: capabilities, centered: centered, actions: actions, keyActionsBuilder: keyActionsBuilder, diff --git a/lib/fido/views/fingerprints_screen.dart b/lib/fido/views/fingerprints_screen.dart index 681a36ef..f551c08e 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, - capability: Capability.fido2, + capabilities: const [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, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: '${l10n.s_fingerprints_get_started} (1/2)', message: l10n.p_set_fingerprints_desc, keyActionsBuilder: hasActions ? _buildActions : null, @@ -116,7 +116,7 @@ class _FidoLockedPage extends ConsumerWidget { if (state.forcePinChange) { return MessagePage( title: l10n.s_fingerprints, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: l10n.s_pin_change_required, message: l10n.l_pin_change_required_desc, keyActionsBuilder: hasActions ? _buildActions : null, @@ -137,7 +137,7 @@ class _FidoLockedPage extends ConsumerWidget { return AppPage( title: l10n.s_fingerprints, - capability: Capability.fido2, + capabilities: const [Capability.fido2], keyActionsBuilder: hasActions ? _buildActions : null, builder: (context, _) => Column( children: [ @@ -190,7 +190,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { ) ], title: l10n.s_fingerprints, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: '${l10n.s_fingerprints_get_started} (2/2)', message: l10n.l_add_one_or_more_fps, keyActionsBuilder: hasActions @@ -250,7 +250,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { }, builder: (context) => AppPage( title: l10n.s_fingerprints, - capability: Capability.fido2, + capabilities: const [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 b9e2e50b..4b9091c5 100644 --- a/lib/fido/views/passkeys_screen.dart +++ b/lib/fido/views/passkeys_screen.dart @@ -61,7 +61,7 @@ class PasskeysScreen extends ConsumerWidget { if (Capability.fido2.value & enabled == 0) { return MessagePage( title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: l10n.s_fido_disabled, message: l10n.l_webauthn_req_fido2, ); @@ -94,7 +94,7 @@ class _FidoLockedPage extends ConsumerWidget { if (!state.hasPin) { return MessagePage( title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: state.credMgmt ? l10n.l_no_discoverable_accounts : l10n.l_ready_to_use, @@ -107,7 +107,7 @@ class _FidoLockedPage extends ConsumerWidget { if (!state.credMgmt && state.bioEnroll == null) { return MessagePage( title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: l10n.l_ready_to_use, message: l10n.l_register_sk_on_websites, keyActionsBuilder: hasActions ? _buildActions : null, @@ -129,7 +129,7 @@ class _FidoLockedPage extends ConsumerWidget { ) ], title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: l10n.s_pin_change_required, message: l10n.l_pin_change_required_desc, keyActionsBuilder: hasActions ? _buildActions : null, @@ -139,7 +139,7 @@ class _FidoLockedPage extends ConsumerWidget { return AppPage( title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [Capability.fido2], keyActionsBuilder: hasActions ? _buildActions : null, builder: (context, _) => Column( children: [ @@ -177,7 +177,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { // TODO: Special handling for credMgmt not supported return MessagePage( title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: l10n.l_no_discoverable_accounts, message: l10n.l_register_sk_on_websites, keyActionsBuilder: hasActions @@ -197,7 +197,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { if (credentials.isEmpty) { return MessagePage( title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [Capability.fido2], header: l10n.l_no_discoverable_accounts, message: l10n.l_register_sk_on_websites, keyActionsBuilder: hasActions @@ -246,7 +246,7 @@ class _FidoUnlockedPageState extends ConsumerState<_FidoUnlockedPage> { }, builder: (context) => AppPage( title: l10n.s_passkeys, - capability: Capability.fido2, + capabilities: const [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 79263c9f..2efbc1b0 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, - capability: Capability.u2f, + capabilities: const [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 11ac356f..b82e53a5 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, - capability: Capability.oath, + capabilities: const [Capability.oath], keyActionsBuilder: hasActions ? (context) => oathBuildActions(context, devicePath, oathState, ref) : null, @@ -175,7 +175,7 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { ) ], title: l10n.s_accounts, - capability: Capability.oath, + capabilities: const [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, - capability: Capability.oath, + capabilities: const [Capability.oath], keyActionsBuilder: hasActions ? (context) => oathBuildActions( context, diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index 6107ea49..a813e03c 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, - capability: Capability.otp, + capabilities: const [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 14cf4524..c5db15a1 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, - capability: Capability.piv, + capabilities: const [Capability.piv], detailViewBuilder: selected != null ? (context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch,