critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

AuthState sealed class/enum exposes exactly four states: authenticated(user), unauthenticated, loading, error(message) with no additional states
Auth Session Manager exposes a Stream<AuthState> that emits the current state immediately upon subscription (BehaviorSubject semantics)
Riverpod StreamProvider<AuthState> wraps the stream and is accessible via ref.watch() without any additional glue code
BLoC-compatible: stream can be added to a StreamSubscription in any BLoC's constructor and cancelled in close()
Transitioning from authenticated → unauthenticated (sign-out or session expiry) emits unauthenticated state within 500ms
Transitioning from unauthenticated → loading → authenticated on successful login emits all intermediate states in order
Error state includes a typed error code distinguishing network errors from token expiry from provider-specific errors
Stream does not emit duplicate consecutive states (e.g., authenticated → authenticated is suppressed with distinctUntilChanged)
Stream is closed and all subscriptions cancelled when AuthSessionManager is disposed
No polling: state changes are push-based only, driven by Supabase Auth onAuthStateChange callbacks
Unit tests cover all four state transitions with mock Supabase auth events
Widget tests confirm that a Consumer/BlocBuilder rebuilds exactly once per state change

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
dart:async
apis
Supabase Auth onAuthStateChange stream
performance requirements
State emission latency under 500ms from underlying Supabase event to UI rebuild
Zero memory leaks: StreamController closed on dispose, no dangling subscriptions
Stream uses broadcast() only if multiple simultaneous listeners are expected; otherwise single-subscription with replay semantics via BehaviorSubject
security requirements
AuthState payloads must never include raw JWT tokens — only opaque user identity references
Error state messages must not expose internal Supabase error details to the UI layer
Stream must not persist state to disk; in-memory only

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use a StreamController.broadcast() if multiple BLoCs/providers may listen simultaneously, or wrap in rxdart BehaviorSubject to replay the latest state to new subscribers. Map Supabase's AuthChangeEvent enum directly: SIGNED_IN → authenticated, SIGNED_OUT → unauthenticated, TOKEN_REFRESHED → stays authenticated (no emission), USER_DELETED → unauthenticated. Apply distinctUntilChanged() at the stream level to prevent redundant rebuilds. The Riverpod provider should be a StreamProvider (not FutureProvider) so it holds AsyncData/AsyncLoading/AsyncError states automatically.

For BLoC consumers, expose the raw Stream so BLoC constructors can call stream.listen() in their constructor body and cancel in close(). Avoid exposing the StreamController directly — only expose the stream getter. Define AuthState as a sealed class (Dart 3) with pattern-matching support for exhaustive switch expressions in the UI.

Testing Requirements

Unit tests (flutter_test): test all four AuthState emissions using a mock SupabaseAuth that fires synthetic onAuthStateChange events. Verify distinctUntilChanged suppresses duplicate emissions. Verify stream closes cleanly on dispose. Integration test: instantiate real AuthSessionManager against Supabase test project, trigger sign-in and sign-out, assert stream sequence.

BLoC test: create a dummy AuthBloc that subscribes to the stream and assert its state sequence mirrors AuthState transitions. Riverpod test: use ProviderContainer with overridden StreamProvider, verify ref.watch() returns correct AsyncValue states. Target 100% branch coverage on the AuthState sealed class.

Component
Authentication Session Manager
service medium
Epic Risks (4)
high impact medium prob technical

The PKCE OAuth flow requires the code verifier to survive an app backgrounding during the Vipps redirect, which can trigger OS memory pressure and clear in-memory state. If the verifier is lost between authorization request and callback, the token exchange fails and the user is stranded with a confusing error.

Mitigation & Contingency

Mitigation: Store the PKCE code verifier in AuthTokenStore (Flutter Secure Storage) immediately after generation, before launching the Vipps redirect. Clear it only after a successful or explicitly failed token exchange.

Contingency: If state loss occurs in production, implement a retry flow that generates a new PKCE pair and restarts the authorization URL request, with a user-visible 'Try again' prompt rather than a generic error.

medium impact medium prob technical

Resuming a Supabase session after biometric verification requires the session token to still be valid. If the session has expired in the background (e.g., after a long device offline period), biometric success will not produce a valid session, and the user will see a confusing 'Face ID worked but still logged out' experience.

Mitigation & Contingency

Mitigation: Before presenting the biometric prompt, check session token expiry. If expired, skip biometrics and route directly to full BankID/Vipps re-authentication. Only offer biometric re-auth if the stored refresh token is still within its validity window.

Contingency: If session expiry during biometric flow occurs in production, implement a graceful transition message ('Your session has expired — please log in again') that preserves the user's last-used authentication method preference.

high impact medium prob integration

BankID and Vipps may return different user identifiers (personnummer, phone number, sub claim) that must be correctly linked to an existing Supabase auth user. If the linking logic has edge cases (e.g., user previously registered via email/password), duplicate Supabase accounts may be created.

Mitigation & Contingency

Mitigation: Design the identity linking logic with explicit disambiguation: check for existing users by personnummer before creating a new Supabase identity. Implement the linking via Supabase Edge Function to keep the logic server-side and auditable.

Contingency: Implement an admin-facing account merge tool in the admin portal to resolve duplicate accounts if they occur. Add a Supabase unique constraint on the personnummer field to make duplicates fail loudly rather than silently.

medium impact high prob dependency

The Vipps nin (personnummer) scope requires explicit approval from Vipps as part of the merchant agreement. If this scope approval is not in place before the production release, the Vipps flow will succeed but return no personnummer, making the primary business value (membership data gap fill) non-functional without user-visible error.

Mitigation & Contingency

Mitigation: Apply for Vipps nin scope approval as part of the merchant onboarding process, well before Phase 2 launch. Implement the service to gracefully handle absent nin claims and show users a clear message if personnummer could not be retrieved.

Contingency: If nin scope is delayed, ship the Vipps login flow without personnummer write-back first (delivering login value immediately) and add personnummer sync as a post-approval update with no UI changes required.