high priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

The onboarding prompt is shown exactly once — after the first successful credential login and only if biometric hardware is available and enrolled
After the user taps 'Enable biometrics' or 'Not now', SessionResumeManager persists the preference so the prompt never appears again for that user
If the user taps 'Enable biometrics', the opt-in flow is triggered (biometric enrolment / key registration) and on success the biometric preference is stored as enabled
If the user taps 'Not now' / skip, the preference is stored as declined and biometric prompt is suppressed in all future sessions
All strings (title, description, button labels) are resolved from the dynamic terminology system
Prompt uses only design token colours, spacing, and typography — no hardcoded hex or pixel values
Prompt is rendered as a bottom sheet or modal overlay on top of the post-login home screen, not a separate route
The opt-in check (has hardware, is enrolled, has not been asked before) is performed in the BLoC/Cubit and result emitted as state before the prompt is shown
Widget is accessible: focus moves into the prompt on open, 'Enable biometrics' button is focused first, dismiss is available via screen-reader swipe to 'Not now'
Unit tests cover: first login triggers prompt, second login does not, opt-in stores preference, skip stores declined preference

Technical Requirements

frameworks
Flutter
BLoC/Cubit
flutter_test
apis
SessionResumeManager (read/write biometric preference)
BiometricAuthService (hardware availability check)
data models
BiometricPreference (enum: notAsked, enabled, declined)
UserSession (current user context)
performance requirements
Preference check must complete before the home screen renders to prevent prompt flicker — resolve in the post-login BLoC transition
security requirements
Biometric preference flag must be stored per-user (keyed by user ID), not globally, to prevent cross-user leakage on shared devices
Do not store raw biometric data — only a boolean flag indicating opt-in status
ui components
Modal bottom sheet or overlay widget
AppButton (primary for 'Enable', ghost/text for 'Not now')
Design token typography for benefit explanation text
Semantics with autofocus on primary action button

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Model the one-time check as a state in the post-login Cubit: after successful credential auth, emit a state variant OnboardingBiometricPromptState if conditions are met, and HomeSate otherwise. The widget tree listens to this state and shows the bottom sheet via showModalBottomSheet. Store the preference flag in SecureStorage (via SessionResumeManager) keyed as biometric_preference_{userId} to be per-user safe. The opt-in action should delegate to the same biometric enrolment flow used elsewhere — do not duplicate enrolment logic.

Use WillPopScope or onDismissed callback of the bottom sheet to ensure a 'Not now' is recorded even if the user swipes the sheet down without tapping the button.

Testing Requirements

Write BLoC unit tests: (1) after first credential login with biometrics available and notAsked preference, state includes showBiometricOnboarding=true; (2) after first login with preference=declined, state does not include the prompt; (3) after second login regardless of preference, state does not show prompt. Write widget tests: (1) prompt renders with correct terminology strings; (2) tapping 'Enable' calls SessionResumeManager.setBiometricPreference(enabled) and dismisses; (3) tapping 'Not now' calls setBiometricPreference(declined) and dismisses; (4) prompt does not re-render after preference is stored. Test on TestFlight build that prompt appears exactly once across app restarts.

Component
Biometric Prompt Overlay
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.