critical priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

When a deep-link callback URL arrives with a valid authorization code, VippsAuthService.exchangeCodeForToken() is called with the extracted code
The OAuth state parameter from the callback URL is validated against the value stored during login initiation; a mismatch is treated as a security error and the flow is aborted with a clear error message
On successful token exchange, the screen navigates to the next screen (Personnummer Confirmation or Home) using the Riverpod-managed router without leaving the Vipps screen in the back stack
If the user cancels the Vipps authentication (callback contains error=access_denied), a plain-language message 'You cancelled the Vipps sign-in. Tap to try again.' is displayed with a Retry button
Network failure during token exchange shows 'Could not connect. Please check your internet connection and try again.' with a Retry button
Vipps-side error responses display a message mapped from the error code, with a fallback to a generic 'Something went wrong. Please try again.' message
The 120 s timeout from task-003 triggers an error state with message 'Sign-in timed out. The Vipps session expired. Please try again.'
Each error state shows a Retry button that resets the screen to its initial idle state, and a Cancel button that returns to the method selector
The error message container is accessible: screen readers announce the error automatically when it appears (using Semantics liveRegion)
No authorization codes, tokens, or personal data are written to application logs or crash reporters

Technical Requirements

frameworks
Flutter
Riverpod
apis
Vipps OAuth 2.0 token exchange endpoint
DeepLinkHandler (app-level stream/provider)
Supabase session storage for persisting the token post-exchange
data models
VippsAuthState (idle | loading | awaitingCallback | success | error)
VippsAuthError (cancellation | networkFailure | vippsError | timeout | csrfMismatch)
OAuthCallbackParams (code, state, error, errorDescription)
performance requirements
Deep-link URL parsing must complete in < 50 ms so the UI updates without perceptible lag after the app foregrounds
Token exchange HTTP call should have a 30 s timeout with a single automatic retry on network error
security requirements
CSRF state parameter must be validated before any token exchange is initiated
Authorization code must be consumed (exchanged) exactly once — invalidate the stored code after the first exchange attempt
Tokens must be stored in flutter_secure_storage, not SharedPreferences
Error messages shown to users must not include raw OAuth error codes or stack traces
ui components
Error state container with icon, plain-language message, Retry button, and Cancel button
Semantics with liveRegion: true on the error message widget for automatic screen reader announcement
Success navigation logic (no visible UI — transparent transition)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The deep-link handler should be a StreamProvider or a global stream that the VippsAuthStateNotifier subscribes to via ref.listen. When the notifier receives the stream event it cancels the timeout timer, validates the CSRF state, and proceeds to token exchange. Structure error types as a sealed class or enum so the UI can switch exhaustively over all cases without a catch-all that silently swallows unexpected states. The Retry action on the error UI should call notifier.reset() which sets state back to idle without navigating — the user then re-taps the CTA button.

This ensures a fresh CSRF state is generated for each attempt. Do not use context.go/pushReplacement directly inside the widget's error handling — instead observe the VippsAuthState in a ref.listen and trigger navigation from there, keeping the navigation logic out of error-handler callbacks. For Supabase session storage: after successful token exchange, call VippsAuthService.persistSession() which writes to Supabase auth and triggers the onAuthStateChange listener used by the root router guard.

Testing Requirements

Unit tests for VippsAuthService: (1) valid callback with matching state → token exchange called; (2) callback with mismatched state → error emitted, no token exchange called; (3) callback with error=access_denied → cancellation error emitted; (4) token exchange network timeout → networkFailure error emitted. Widget tests: (1) simulate a successful deep-link arrival and assert navigation occurs; (2) simulate cancellation callback and assert error message text is displayed and Retry button is present; (3) tap Retry and assert screen returns to idle state; (4) tap Cancel and assert router pops back to method selector. Accessibility test: use Flutter's SemanticsController to assert the error message widget has liveRegion set to true. Security test: assert no authorization code appears in any print/log output (via log capture in test).

Component
Vipps Authentication Screen
ui medium
Epic Risks (3)
high impact medium prob technical

BankID on mobile uses a WebView or external app redirect that has known compatibility issues with Flutter's WebView package on certain Android versions. BankID's JavaScript-heavy broker pages may also trigger CSP or mixed-content errors in a Flutter WebView, preventing the authentication flow from completing.

Mitigation & Contingency

Mitigation: Use the flutter_inappwebview package (more mature than webview_flutter for complex OAuth pages) and validate BankID WebView rendering on the broker's test environment before integrating with the service layer. Prefer external browser redirect where the broker supports it.

Contingency: If WebView approach fails for certain BankID brokers, implement the full external browser redirect + deep link callback pattern as the primary flow and treat WebView as a fallback only.

medium impact medium prob technical

The OAuth redirect flows (both Vipps and BankID) temporarily move the user outside the Flutter app into an external browser or the Vipps/BankID app. Screen reader users may lose focus context during this transition and become disoriented when the app callback returns them to the loading state, failing the WCAG 2.2 AA mandate.

Mitigation & Contingency

Mitigation: Implement explicit accessibility announcements (live region announcements) at each transition point: when launching the external flow ('Opening Vipps'), during the loading wait state ('Waiting for Vipps confirmation'), and on return ('Login successful' or 'Login failed — please try again'). Test with VoiceOver on iOS and TalkBack on Android during development.

Contingency: If OAuth transition accessibility is unresolvable on a specific platform, add an explicit accessibility user guide in the onboarding flow explaining the external app redirect behavior to set user expectations.

low impact high prob technical

Biometric UI varies significantly across devices — Face ID (iPhone), fingerprint sensor (most Android), front-facing camera biometrics (some Android), and devices with no biometrics at all. Flutter's local_auth handles the OS dialog but the surrounding UI must gracefully handle all these cases, and testing coverage for all permutations is difficult.

Mitigation & Contingency

Mitigation: Use local_auth's getAvailableBiometrics() to detect the exact biometric type and render appropriate iconography (Face ID icon vs. fingerprint icon). For devices with no biometrics, skip the biometric screen entirely and route directly to full re-authentication.

Contingency: If a specific device configuration produces unexpected local_auth behavior in production, implement a user-accessible toggle in Settings to disable biometric login entirely, routing those users to the standard BankID/Vipps flow without biometrics.