high priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

A toggle row appears in the app settings screen under an 'Authentication' or equivalent section header
Toggle label is resolved from the dynamic terminology system (e.g. 'Use Face ID / fingerprint to log in')
Toggle initial state reflects the current persisted biometric preference from SessionResumeManager
When biometric hardware is unavailable or not enrolled, the toggle is disabled (grey, non-interactive) and BiometricUnavailableBanner is shown below it with the correct reason
When toggle is switched on, the opt-in flow is invoked; if opt-in fails, the toggle reverts to off and an error message is shown
When toggle is switched off, a confirmation dialog (or inline confirmation row) asks the user to confirm before invoking the revocation flow
After confirmed revocation, the toggle is off and BiometricUnavailableBanner is hidden (hardware is still available, just not opted in)
Toggle state is not updated in the UI until the underlying async operation (opt-in or revocation) completes successfully — no optimistic UI update
All interactive elements are accessible with correct Semantics roles and labels
Widget tests cover: toggle disabled when hardware absent, toggle reflects saved preference, toggling on triggers opt-in, toggling off with confirmation triggers revocation

Technical Requirements

frameworks
Flutter
BLoC/Cubit
flutter_test
apis
SessionResumeManager (read/write biometric preference)
BiometricAuthService (hardware availability check)
Revocation flow (task-011 output)
data models
BiometricPreference (enum: notAsked, enabled, declined)
BiometricUnavailableReason (enum)
performance requirements
Settings screen must load within 200ms — biometric capability check should be pre-fetched by the settings BLoC, not on toggle render
security requirements
Confirm revocation intent before clearing credentials to prevent accidental lockout
Do not store the biometric toggle state in plain SharedPreferences — use SessionResumeManager which routes to SecureStorage
ui components
SwitchListTile or custom toggle row using design tokens
BiometricUnavailableBanner (task-007)
Confirmation dialog (AppDialog or bottom sheet) before revocation
Semantics label on the Switch for screen readers

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Model the biometric settings section as a dedicated Cubit (BiometricSettingsCubit) rather than mixing state into the general settings Cubit. On init, load BiometricPreference and BiometricUnavailableReason in parallel. Pass the reason to BiometricUnavailableBanner and conditionally render it based on hardware availability (not on opt-in status). The toggle widget itself should be stateless — drive it entirely from BLoC state.

Treat the toggle as a command dispatcher: toggling on emits EnableBiometricsRequested event, toggling off emits DisableBiometricsRequested event. The BLoC handles confirmation logic or delegates it. Ensure the revocation flow (task-011) is injectable so this widget can call it without direct coupling.

Testing Requirements

Write BLoC/Cubit unit tests: (1) initial state loads biometric preference and hardware status; (2) when hardware absent, state emits biometricDisabled with correct reason; (3) toggle-on action triggers opt-in call and emits biometricEnabled on success; (4) toggle-on action emits error state on opt-in failure and reverts preference. Write widget tests: (1) toggle is disabled and BiometricUnavailableBanner is rendered when hardware absent; (2) toggle shows correct initial value from preference; (3) tapping toggle calls the correct BLoC event; (4) confirmation dialog appears before revocation; (5) cancelling confirmation leaves toggle on.

Component
Biometric Unavailable Banner
ui low
Epic Risks (3)
medium impact high prob technical

Flutter's AppLifecycleState.resumed event fires in scenarios beyond simple user-initiated app-switching: it also fires when the native biometric dialog itself dismisses and returns control to Flutter, when system alerts (low battery, notifications) temporarily cover the app, and when the app is foregrounded by a deep link. Without careful debouncing and state tracking, SessionResumeManager can trigger multiple overlapping biometric prompts or prompt immediately after a just-completed authentication, creating a confusing loop.

Mitigation & Contingency

Mitigation: Implement a state machine inside SessionResumeManager with explicit states (idle, prompting, authenticated, awaiting-fallback) and guard all prompt triggers with a state check. Add a minimum inter-prompt interval of 3 seconds. Write widget tests that simulate rapid lifecycle event sequences and verify only one prompt is shown.

Contingency: If the state machine approach proves difficult to test or maintain, replace it with a simple boolean isPromptActive flag with a debounce timer, accepting slightly less precise semantics in exchange for simpler reasoning about concurrent lifecycle events.

high impact medium prob technical

The BiometricPromptOverlay appears on top of the existing app content when the app resumes. If focus is not correctly transferred to the overlay and returned to the underlying screen after authentication, screen reader users (VoiceOver/TalkBack) will either be unable to interact with the prompt or will lose their navigation position in the app after authentication completes, violating WCAG 2.2 focus management requirements and creating a broken experience for Blindeforbundet users.

Mitigation & Contingency

Mitigation: Use Flutter's FocusScope and FocusTrap utilities to capture focus within the overlay on presentation and restore the previous FocusNode after dismissal. Add a live region announcement (using the accessibility live region announcer component) when the overlay appears. Include dedicated VoiceOver and TalkBack test cases in the acceptance criteria.

Contingency: If FocusTrap behaviour proves unreliable across Flutter versions, implement the overlay as a full Navigator push to a modal route rather than an overlay widget, which gives Flutter's built-in modal semantics and focus management automatic WCAG-compliant behaviour.

low impact high prob technical

The BiometricUnavailableBanner needs to deep-link to biometric enrollment settings on both iOS and Android. iOS uses a single URL scheme (app-settings:) that opens the app's settings page. Android has no universal URL for biometric settings — the correct Intent action (Settings.ACTION_BIOMETRIC_ENROLL) was introduced in API 30, with different fallback actions required for API 23-29. Using the wrong action on older Android devices either crashes or navigates to an unrelated settings screen.

Mitigation & Contingency

Mitigation: Build an Android API version check into LocalAuthIntegration that selects the correct Intent action based on the runtime SDK version. Test against Android API 23, 28, and 30+ in the CI matrix. For iOS, validate that the app-settings: URL scheme is correctly declared.

Contingency: If the Android settings Intent fragmentation cannot be resolved reliably for all target API levels, fall back to navigating to the top-level Settings screen (Settings.ACTION_SETTINGS) with an overlay instruction telling the user to navigate to 'Security > Biometrics' manually, ensuring the user always has a path to resolve the issue even if the deep link is imprecise.