critical priority high complexity backend pending integration specialist Tier 4

Acceptance Criteria

Assertion polling implemented with a 2-second interval, starting after the BankID deep link is launched
Polling calls POST /functions/v1/bankid-collect with the sessionId from secure storage; max 90 poll attempts (3 minutes total)
Edge Function returns one of: pending, complete(identity), failed(reason), cancelled
On 'complete', the validated national identity (personnummer + name) is received and passed to the BankID user persistence layer
On 'failed' or 'cancelled', AuthState.error or AuthState.unauthenticated is emitted respectively and sessionId is cleared
On polling timeout (90 attempts exhausted), AuthState.error is emitted with timeout reason
Certificate validation is performed exclusively server-side in the Edge Function — client never receives raw BankID certificates
Polling stops immediately when the app goes to background (AppLifecycleState.paused) and resumes when foregrounded (AppLifecycleState.resumed)
If the deep link callback is received instead of polling (push-based), polling is cancelled immediately in favor of the callback
Personnummer extracted from the validated assertion is held in memory only — never logged or written to client-side storage
AuthState transitions to authenticated(user) after successful Supabase session establishment post-validation
Unit tests cover pending→complete, pending→failed, pending→cancelled, and timeout transitions

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
flutter_secure_storage
dart:async (Timer.periodic)
apis
Supabase Edge Functions (Deno) bankid-collect
BankID REST API (server-side only)
performance requirements
Polling interval exactly 2 seconds to match BankID API rate limits
Transition from 'complete' response to AuthState.authenticated in under 1 second
Background polling suspension prevents unnecessary battery drain and network usage
security requirements
BankID certificate validation must occur exclusively server-side — no certificate material transmitted to mobile
SessionId sent in polling requests validated by the Edge Function against the JWT claims to prevent sessionId spoofing
Personnummer received from Edge Function never written to SharedPreferences, files, or analytics
Edge Function validates that the collecting JWT belongs to the same user who initiated the session
All polling requests made over TLS only
BankID assertion payload never forwarded to any third-party service or logging system
ui components
BankIDWaitingScreen (spinner + cancel button + status message)
ErrorDialog (on failure/timeout)
SuccessTransition (on complete)

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Implement polling as a Dart Timer.periodic(Duration(seconds: 2), ...) that calls the Edge Function and processes the response. Wrap the poll counter in a final counter variable; cancel the timer when counter >= 90 or when a terminal state (complete/failed/cancelled) is received. Use WidgetsBindingObserver to implement AppLifecycleState monitoring — pause the timer on paused and restart on resumed. The Edge Function POST /bankid-collect should call the BankID /collect endpoint with the orderRef, validate the certificate chain, extract the personnummer from the completionData.user.personalNumber field, and return only the extracted identity fields (no raw BankID objects).

After receiving 'complete', immediately cancel the polling timer, clear sessionId from flutter_secure_storage, and pass the identity to the same Supabase user persistence logic used by Vipps (task-006). Reuse the consent dialog for NIN storage consent — the UX should be consistent between Vipps and BankID login paths. Handle BankID-specific error codes: 'expiredTransaction', 'certificateErr', 'userCancel', 'cancelled', 'startFailed' each mapped to appropriate AuthState.error sub-types.

Testing Requirements

Unit tests (flutter_test): mock Timer.periodic and Edge Function responses to test the full polling state machine. Verify polling stops at 90 attempts and emits AuthState.error. Verify 'cancelled' response emits AuthState.unauthenticated. Verify 'complete' response triggers the downstream persistence layer (mock).

Verify polling suspends on AppLifecycleState.paused and resumes on AppLifecycleState.resumed. Verify early termination when deep link callback received. Integration tests: run against BankID sandbox environment, simulate user completing authentication in BankID test app, verify end-to-end flow completes. Security tests: attempt to poll with a mismatched sessionId, verify Edge Function rejects it.

Verify no BankID certificate material is present in the Flutter response objects. Target 95%+ branch coverage on the polling state machine.

Component
BankID Authentication Service
service high
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.