critical priority high complexity backend pending backend specialist Tier 5

Acceptance Criteria

BiometricAuthBloc exposes sealed BiometricAuthState with variants: Idle, Prompting, Success, Failed(retryCount), Unavailable(reason), FallbackRequired
BiometricAuthEvent.authenticate triggers transition from Idle → Prompting → (Success | Failed | Unavailable | FallbackRequired)
BiometricAuthEvent.reset transitions any state back to Idle
Failed state carries retryCount int and is re-emitted with incremented count on each failed attempt
Unavailable state carries UnavailableReason enum (hardwareMissing, notEnrolled, permissionDenied) for UI differentiation
FallbackRequired state is emitted when lockout threshold is crossed and carries no retry option
Bloc does not emit duplicate consecutive identical states (use equatable or manual guard)
BiometricAuthScreen and BiometricPromptOverlay can independently BlocBuilder-subscribe to relevant state subsets
Accessibility: screen reader announcement triggered on state transition to Prompting (VoiceOver/TalkBack)
All state transitions are covered by bloc_test unit tests
Bloc is registered in the DI container (Riverpod provider or GetIt) and injectable into both UI components

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Riverpod
Equatable
apis
BiometricAuthService (task-005)
flutter_local_auth
performance requirements
State transitions must be synchronous where possible — async only for the authenticate() service call
Bloc must not hold strong references to UI widgets — state objects are pure data
security requirements
BiometricAuthState.Success must not carry the Session object — session is managed by SupabaseSessionManager, not exposed in UI state
Failed state must not expose raw platform error messages in accessible state fields
Bloc must be scoped to the authentication screen lifetime — not a global singleton — to prevent stale state leaking between sessions
ui components
BiometricAuthScreen
BiometricPromptOverlay

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use Cubit over full Bloc if the event set is simple (≤5 events) — Cubit reduces boilerplate. Seal states using Dart 3 sealed classes or the freezed package for exhaustive pattern matching in UI. Keep the Bloc thin: delegate all business logic to BiometricAuthService — the Bloc only orchestrates state transitions. Use Equatable on all state classes to prevent unnecessary UI rebuilds.

For the accessibility announcement, trigger a platform channel call or use the semantics framework in the Prompting entry action. Scope the Bloc provider to the auth route using BlocProvider scoped to the navigator subtree — do not place it at the app root. Consider emitting a transient BiometricAuthState.tokenRefreshing sub-state between Prompting and Success if the token refresh is slow, to give the UI a loading indicator opportunity.

Testing Requirements

Use bloc_test package for all state machine tests. Test every valid transition path: Idle→Prompting→Success, Idle→Prompting→Failed(1), Failed(1)→Prompting→Failed(2), Idle→Unavailable, Prompting→FallbackRequired. Test reset event from every non-Idle state. Test that duplicate state emission is suppressed.

Test accessibility announcement side effect fires on Prompting transition. Aim for 100% state/event coverage. Use mockito or mocktail to mock BiometricAuthService.

Component
Biometric Authentication Service
service medium
Epic Risks (3)
high impact medium prob technical

Multiple concurrent callers (e.g., SessionResumeManager and a background sync service) could simultaneously detect a near-expired token and each invoke SupabaseSessionManager.refreshSession(), causing duplicate refresh API calls and potentially a token invalidation race condition on the Supabase Auth server. This can result in one caller receiving a valid refreshed token while another receives a 401, causing intermittent authentication failures.

Mitigation & Contingency

Mitigation: Implement a single-flight pattern inside SupabaseSessionManager so that concurrent refresh calls coalesce into one in-flight request. Use a Dart Completer or AsyncMemoizer to ensure all waiters receive the same refreshed token. Write a concurrent integration test to validate the single-flight behaviour.

Contingency: If the single-flight pattern introduces deadlocks or timeout complexity, fall back to a mutex-based lock with a 10-second timeout, logging a warning if the lock is held longer than expected, and triggering a full re-login if the refresh ultimately fails.

high impact low prob security

Supabase Row-Level Security policies evaluate the JWT claims (user_id, role, org_id) on every query. If the refreshed token contains stale or changed claims — for example if a coordinator's role was updated server-side — RLS may silently block data access even though the session appears valid from the client's perspective, causing confusing empty screens rather than an authentication error.

Mitigation & Contingency

Mitigation: After every token refresh, decode the new JWT and compare key claims (role, org_id) with the cached values. If claims have changed, emit a session-claims-changed event that triggers a role re-resolution and navigation reset. Document this behaviour in the SupabaseSessionManager API contract.

Contingency: If claims drift is detected in production and causes data visibility issues, provide a force-refresh mechanism in the UI (pull-to-refresh on home screen) that clears cached role state and re-fetches from Supabase, accompanied by a user-visible toast indicating the session was refreshed.

medium impact medium prob security

Allowing session resumption from cached local token when offline introduces a window where a revoked or invalidated session can still grant app access. For example, if a coordinator deactivates a peer mentor's account while the mentor is offline, the mentor continues to have access until connectivity is restored and the token is validated server-side.

Mitigation & Contingency

Mitigation: Set a maximum offline grace period (e.g., 24 hours) stored alongside the token in SecureSessionStorage. If the grace period is exceeded, force a full credential re-login regardless of connectivity status. Scope offline access to read-only operations only, requiring connectivity for any write that reaches Supabase.

Contingency: If the offline grace period logic is found to be insufficient for compliance, implement remote session invalidation via a lightweight push notification that clears SecureSessionStorage even when the app is backgrounded, using FCM with a data-only message.