critical priority medium complexity backend pending backend specialist Tier 0

Acceptance Criteria

SessionResumeManager implements WidgetsBindingObserver and is registered with WidgetsBinding.instance.addObserver in its initializer or constructor
SessionResumeManager is disposed correctly: removeObserver called in dispose() to prevent memory leaks
didChangeAppLifecycleState fires and logs (debug-only) on every AppLifecycleState transition
Only AppLifecycleState.resumed events trigger session resume logic; all other states are explicitly ignored
BiometricAuthService and SecureSessionStorage are injected via constructor parameters (no hard instantiation inside the class)
SessionResumeManager can be instantiated in unit tests by injecting mock implementations of both dependencies
No Riverpod or BLoC state is read or written inside this task — only the class skeleton and lifecycle hook; state integration is handled in subsequent tasks
Class is registered as a singleton in the Riverpod provider graph so only one observer is active at a time

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
flutter_local_auth (iOS LocalAuthentication / Android BiometricPrompt)
Supabase Auth
performance requirements
didChangeAppLifecycleState callback must return in under 1ms for non-resumed states — use early return guard
Observer registration must occur exactly once per app lifecycle to avoid duplicate callbacks
security requirements
Session resume logic must only be triggered from the resumed lifecycle event, not from inactive or paused states
BiometricAuthService dependency must be the injected abstraction — concrete flutter_local_auth calls must not appear in SessionResumeManager directly

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use a `final _binding = WidgetsBinding.instance` reference stored in the constructor rather than accessing the global repeatedly. The class should expose a single `_onAppResumed()` private method that subsequent tasks (task-002, task-003) will populate — leave it as an empty async method stub in this task. Register the Riverpod provider as `Provider` with `keepAlive: true` to ensure the observer is never garbage collected while the app is running. Be aware that on app cold start, AppLifecycleState.resumed fires before the widget tree is fully built — guard against this by checking if the Riverpod container is fully initialized before acting.

Do not use a GlobalKey or BuildContext inside this class; communicate results upward through a Riverpod StateNotifier or stream instead.

Testing Requirements

Unit tests using flutter_test and mockito (or mocktail). Test cases: (1) observer is registered on construction and deregistered on dispose; (2) didChangeAppLifecycleState with AppLifecycleState.resumed calls the resume handler; (3) didChangeAppLifecycleState with paused, inactive, detached, and hidden states does NOT call the resume handler; (4) injecting mock BiometricAuthService and SecureSessionStorage works without errors. Use FakeWidgetsBindingObserver pattern to simulate lifecycle events in tests without launching a full Flutter app.

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.