high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

Revocation flow clears: (1) biometric-encrypted session token from SecureSessionStorage, (2) biometric preference flag, (3) any locally cached auth state
Revocation flow calls Supabase signOut() to invalidate the server-side session token before clearing local storage
If Supabase signOut() fails (network error), the flow still clears local credentials and proceeds — remote session expiry is acceptable as fallback
The entire local clear operation is executed atomically: if any SecureSessionStorage write fails mid-flow, a rollback attempt is made and an error is surfaced rather than leaving credentials in a partially cleared state
After successful revocation, the app navigates to the credential login screen (not to the onboarding/organisation selection screen)
A confirmation message is displayed on the credential login screen (e.g. via a banner or snackbar) stating that biometric login has been disabled
Revocation does not delete the user's account or non-biometric session preferences
SessionResumeManager exposes a single revokeAndSignOut() method that encapsulates the full flow
Unit tests cover: successful full revocation, Supabase failure with local clear fallback, partial local failure with rollback
The flow is called correctly from the biometric settings toggle (task-010) via the BLoC

Technical Requirements

frameworks
Flutter
BLoC/Cubit
flutter_test
apis
Supabase auth.signOut()
SecureSessionStorage (delete biometric token key)
SessionResumeManager (clear biometric preference)
data models
UserSession (active session reference)
BiometricPreference (enum)
SecureStorageKeys (key constants for biometric token and preference)
performance requirements
Revocation flow must complete within 3 seconds including the Supabase sign-out network call
UI must show a loading indicator for the duration of the async flow to prevent double-taps
security requirements
Biometric-encrypted keys must be deleted using the secure storage provider's delete API, not overwritten with empty strings — verify the key is fully removed
Supabase access and refresh tokens must be invalidated server-side, not just deleted locally, to prevent replay attacks
Do not log credential values at any point in the revocation flow — log event names only (e.g. 'biometric_revocation_started')
Apply a write lock (mutex or sequential queue) around the atomic clear sequence to prevent concurrent revocation calls
ui components
Loading overlay or progress indicator during revocation
SnackBar or banner on credential login screen confirming successful revocation

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement the atomic local clear using a try/catch/rollback pattern: collect all storage keys to delete, attempt each deletion in sequence, and if any fails, re-write the previously deleted keys back (best-effort rollback). Log the failure and surface a RevocationException. Use a Completer-based mutex (or a simple bool _isRevoking flag with early return) to prevent concurrent invocations. Separate the Supabase sign-out call from the local storage clear so that network failures only affect remote invalidation.

The navigation to credential login screen should happen via a GoRouter redirect triggered by the BLoC emitting an unauthenticated state, rather than calling Navigator.pushNamedAndRemoveUntil directly — this keeps navigation logic in the router and prevents duplicate navigation events.

Testing Requirements

Write unit tests for SessionResumeManager.revokeAndSignOut(): (1) happy path — all steps succeed, assert all storage keys deleted and signOut called; (2) Supabase signOut throws NetworkException — assert local storage is still cleared and flow completes without rethrowing; (3) SecureSessionStorage.delete throws on biometric token key — assert rollback is attempted and a RevocationException is thrown; (4) concurrent call protection — calling revokeAndSignOut twice simultaneously should not result in a double sign-out. Write BLoC tests verifying the settings toggle invokes revokeAndSignOut and handles RevocationException by emitting an error state. Write integration test on TestFlight build: disable biometrics, restart app, verify login screen appears and biometric prompt is not shown.

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.