high priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

A debounce timer with a configurable window (default 500ms, injectable via constructor for testability) prevents duplicate calls to _onAppResumed within the window
A boolean flag `_isAuthInFlight` is set to true when an authentication attempt begins and false when it completes (success, failure, or error)
If _isAuthInFlight is true when a new resumed event fires, the new event is silently dropped — no second biometric prompt is shown
Rapid toggle test: simulating 5 resumed events within 200ms results in exactly 1 call to the authentication handler
Debounce timer is cancelled in dispose() to prevent callbacks firing on a disposed object
The debounce window is exposed as a constructor parameter (`Duration debounceDuration`) with a default of 500ms so tests can use Duration.zero for synchronous testing
_isAuthInFlight is reset to false even if the authentication attempt throws an unhandled exception (use try/finally)

Technical Requirements

frameworks
Flutter
Dart
Riverpod
performance requirements
Debounce implementation must use dart:async Timer, not Future.delayed, to allow cancellation
No additional memory allocations per lifecycle event beyond resetting the existing timer reference
security requirements
_isAuthInFlight flag prevents race conditions that could lead to concurrent biometric dialogs revealing session state to an observer
Timer cancellation in dispose() ensures no callbacks fire after the app is fully backgrounded or terminated

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement debounce using a nullable `Timer? _debounceTimer` field. In the resumed handler: cancel existing timer if non-null, then start a new timer. Inside the timer callback, check `_isAuthInFlight` before proceeding.

Use `try/finally` around the auth call to guarantee `_isAuthInFlight = false` on exit. Keep the debounce logic in a single private method `_scheduleAuthCheck()` so it is easy to unit test in isolation. Note: 500ms is the minimum needed to absorb the system-generated rapid resume/pause cycle that occurs on some Android devices when the biometric dialog itself causes a lifecycle transition — do not reduce below 300ms without testing on Android. On iOS, rapidly switching from the app to the system biometric sheet and back can also fire duplicate resumed events.

Testing Requirements

Unit tests using flutter_test with fake async support (`FakeAsync` from `fake_async` package). Test cases: (1) single resumed event triggers handler exactly once; (2) two resumed events within debounce window trigger handler exactly once; (3) two resumed events outside debounce window trigger handler twice; (4) _isAuthInFlight=true blocks a concurrent resumed event; (5) _isAuthInFlight is reset to false after successful auth; (6) _isAuthInFlight is reset to false after auth throws exception (try/finally path); (7) dispose() cancels any pending timer. All tests must be deterministic — use FakeAsync to control time rather than real delays.

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.