Integrate BiometricAuthService into BiometricPromptOverlay
epic-biometric-session-authentication-ui-and-lifecycle-task-005 — Wire the native biometric dialog trigger into BiometricPromptOverlay by calling BiometricAuthService on widget mount (initState/didChangeDependencies). Handle the three result states: success (dismiss overlay and resume app), failure (show retry option), and fallback (navigate to credential login). Support Face ID on iOS and fingerprint on Android.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Inject BiometricAuthService via the constructor (or Riverpod ref.read in initState if the service is already in the Riverpod graph) — do not use a static instance. Use `WidgetsBinding.instance.addPostFrameCallback` inside initState to trigger the first authentication attempt after the overlay is fully rendered; calling authenticate() directly in initState can cause issues if the widget tree is not yet laid out. Model widget state with a private `_BiometricOverlayState` enum: `idle`, `authenticating`, `failed`, `maxAttemptsReached` — use `setState` to transition between them. For the retry counter, use a simple `int _attemptCount` field.
The fallback button (from task-004) should also call onFallbackTapped — wire it in this task. Ensure that if the widget is disposed while authenticate() is in progress (e.g. user force-quits), the future result is discarded safely using a `_mounted` guard (`if (!mounted) return`) before calling setState or callbacks.
Testing Requirements
Widget tests using flutter_test with a mocked BiometricAuthService. Test cases: (1) authenticate() called on mount (verify mock receives exactly one call); (2) success result → onAuthSuccess callback fired and widget popped; (3) failure result → retry button appears; (4) retry button tap → authenticate() called again; (5) 3 failures → onFallbackTapped callback fired automatically; (6) fallback result from service → onFallbackTapped fired immediately; (7) authenticate() throws exception → onFallbackTapped fired; (8) loading indicator visible while authenticate() future is pending (use Completer to hold future). Integration test on a physical device (TestFlight build): verify Face ID dialog appears on app resume, success returns to app, cancel routes to login.
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.
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.
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.