critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

On resumed event, SecureSessionStorage.readSession() is called and the result is awaited before any routing decision
If the session is null or missing, the user is routed to the full credential login screen with no error shown (clean state)
If the session expiry timestamp is in the past (expired), the stored session is deleted from SecureSessionStorage and the user is routed to credential login
If the session is valid (non-expired) and BiometricAuthService.isAvailable() returns true, BiometricPromptOverlay is shown
If the session is valid but BiometricAuthService.isAvailable() returns false, the user is routed directly to credential login without showing a biometric prompt
All routing is performed via GoRouter (or the project's established navigation service) — no direct Navigator.pushNamed calls
Session expiry check uses UTC timestamps to avoid timezone-related false expirations
Expired session deletion is completed before navigation to ensure the login screen cannot resume the expired session

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase Auth
flutter_local_auth (iOS LocalAuthentication / Android BiometricPrompt)
performance requirements
Session read and expiry check must complete in under 100ms; flutter_secure_storage reads are synchronous per key and should not block the UI thread materially
Routing decision must be made within one async frame after the resumed event fires
security requirements
Expired session must be deleted from SecureSessionStorage before routing — do not leave stale tokens in secure storage
Routing to BiometricPromptOverlay must not expose any session token data in the route arguments
UTC timestamp comparison must be used for expiry checks — DateTime.now().toUtc() vs stored UTC expiry
BiometricAuthService.isAvailable() must be awaited fresh on each resume — do not cache availability across resumes as device enrollment can change

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define a private `_resolveResume()` method that encapsulates the full validation and routing logic, called from within the debounce timer callback (task-002). Keep routing logic thin: resolve a `SessionResumeDestination` enum value (biometricPrompt, credentialLogin) from the validation, then dispatch a single navigation call. This keeps the routing logic testable without a real navigation stack. For expiry parsing, store the expiry as an ISO-8601 UTC string in SecureSessionStorage (e.g.

`2026-03-26T12:00:00Z`) and parse with `DateTime.parse(expiry).toUtc()`. Do not use Unix epoch integers — they are harder to debug. Prefer a Result/Either pattern or typed exceptions for SecureSessionStorage read errors rather than try/catch at the call site, to keep _resolveResume() readable. Ensure the Supabase Auth session expiry field is used as the source of truth for the stored expiry — do not create a separate expiry timestamp.

Testing Requirements

Unit tests using flutter_test with mocked SecureSessionStorage and BiometricAuthService. Test cases: (1) null session → routes to login; (2) missing session key → routes to login; (3) expired session → deletes session then routes to login; (4) valid session + biometrics available → routes to BiometricPromptOverlay; (5) valid session + biometrics unavailable → routes to login; (6) expired session with timezone offset → correctly identifies expiry using UTC; (7) SecureSessionStorage read throws exception → routes to login (defensive fallback). Integration test: boot app with expired token stored in secure storage, put app to background and resume, verify login screen appears and token is cleared.

Component
Session Resume Manager
service medium
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.