high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Widget accepts a required `BiometricUnavailableReason` enum parameter with at least two values: hardwareAbsent and notEnrolled
When reason is hardwareAbsent, banner displays text from the organisation's terminology key biometric_unavailable_no_hardware (e.g. 'This device does not support biometric login')
When reason is notEnrolled, banner displays text from terminology key biometric_unavailable_not_enrolled (e.g. 'No biometric credentials are set up on this device')
All strings are fetched from the dynamic terminology/labels system, not hardcoded
Banner renders as an inline card/banner (not a dialog) with an icon, a title, and a description paragraph
Banner icon reflects severity — uses a warning or info icon from the design token icon set, not a generic close icon
Widget is dismissible only if the parent opts in via an optional onDismiss callback; if not provided, no dismiss button is shown
Component passes flutter_test golden test and widget test asserting correct string per reason code
Widget is accessible: icon is decorative (ExcludeSemantics), text has sufficient contrast per design tokens (WCAG AA)

Technical Requirements

frameworks
Flutter
flutter_test
apis
BiometricAuthService (reason code output)
data models
BiometricUnavailableReason (enum)
performance requirements
Widget must render in a single frame — no async calls inside build()
security requirements
Do not expose raw platform error codes or exception stack traces in the UI; map all codes to user-friendly terminology strings
ui components
Design token colours and typography (AppTextStyle, AppColors)
Organisation labels/terminology system for all strings
Icon from design token icon set
AppButton (optional dismiss action)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Keep the widget stateless — it renders based solely on the reason parameter and terminology lookup. Use the existing organisation labels system (likely a provider or inherited widget) via context to resolve strings. Define BiometricUnavailableReason enum in a shared auth types file to avoid circular imports. Avoid putting business logic in the widget; the caller (BLoC/Cubit) is responsible for determining the reason.

The banner should use the `.card` design token surface colour and the warning accent token for the icon/border to be visually consistent with other alert components in the app.

Testing Requirements

Write widget tests: (1) pump BiometricUnavailableBanner(reason: BiometricUnavailableReason.hardwareAbsent) and assert the correct terminology string is rendered; (2) pump with notEnrolled reason and assert alternative string; (3) pump without onDismiss and assert no dismiss button is present; (4) pump with onDismiss callback, tap dismiss button, and assert callback is invoked. Add golden test for visual regression. Verify all Semantics labels are present using tester.ensureSemantics().

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.