Implement Riverpod session state propagation
epic-biometric-session-authentication-core-services-task-015 — Implement the sessionStateStream in SupabaseSessionManager and wire it to Riverpod providers so that session state changes (authenticated, refreshing, expired, signed-out) are propagated reactively across the app. Riverpod providers consuming this stream automatically update role-based home screens, contact lists, and any session-gated widget without requiring explicit state management calls.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Use a BehaviorSubject (from rxdart) or implement a simple ReplaySubject with a seed value for sessionStateStream — new subscribers must receive the current state immediately to avoid race conditions during widget tree initialization. Bridge Supabase's onAuthStateChange into sessionStateStream by subscribing in SupabaseSessionManager's constructor: map AuthChangeEvent.signedIn → SessionState.authenticated, AuthChangeEvent.signedOut → SessionState.signedOut, AuthChangeEvent.tokenRefreshed → SessionState.authenticated. For Riverpod wiring, create a single sessionStateProvider as a StreamProvider.autoDispose that calls ref.keepAlive() — this ensures it stays alive throughout the app lifecycle but cleans up on test teardown. For cross-provider invalidation on sign out, add a ref.listen(sessionStateProvider, ...) in the Riverpod ProviderContainer setup (in main.dart or app_providers.dart) that calls ref.invalidate() on data providers when signedOut is observed.
Avoid adding this logic to individual feature providers to keep the invalidation centralized. Create a SessionGateWidget that uses ConsumerWidget with ref.watch(sessionStateProvider) and returns the appropriate UI for each state — this reduces boilerplate across screens.
Testing Requirements
Unit tests for stream bridging: (1) Supabase onAuthStateChange emits signedIn event — verify sessionStateStream emits SessionState.authenticated with correct AppUser. (2) Supabase onAuthStateChange emits signedOut event — verify sessionStateStream emits SessionState.signedOut. (3) New subscriber receives last emitted state immediately (BehaviorSubject replay behavior). Widget tests: (4) SessionGateWidget renders child when sessionStateProvider is authenticated.
(5) SessionGateWidget renders expired UI when sessionStateProvider is expired. (6) Role-based home screen rebuilds correctly when authenticated state changes user role. (7) Contact list provider returns empty AsyncError when sessionStateProvider is signedOut. Integration test: full sign-in → refresh → sign-out flow verified via ProviderContainer in test environment.
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.
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.
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.