critical priority high complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

Successful BankID authentication callback results in navigation to the Personnummer Confirmation Widget with the authenticated user context passed as a route argument
The OIDC state parameter from the callback is validated against the stored value before any token exchange proceeds
All BankID documented error codes are mapped to specific English-language user-facing messages (minimum: cancelled, expired, alreadyInProgress, userBlocked, internalError, timeout)
User-initiated Cancel button is visible during the loading/awaiting state and, when tapped, calls BankIDAuthService.cancelSession() to abort the server-side session before navigating back
Cancellation navigates back to the authentication method selector without leaving the BankID screen in the navigation stack
After a timeout (120 s), the error state shows 'Sign-in timed out. The BankID session expired. Please try again.' 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
All error states include both a Retry (resets to idle) and a Cancel (navigates back) option
Error messages are announced by screen readers via Semantics liveRegion
No BankID tokens, personnummer, or error detail codes are written to application logs or crash reporter breadcrumbs

Technical Requirements

frameworks
Flutter
Riverpod
apis
BankID OIDC callback / token endpoint
DeepLinkHandler stream for deep-link callback
Supabase auth session storage post-exchange
data models
BankIDAuthState (idle | launching | waitingForApp | verifying | success | error)
BankIDError (cancelled | expired | alreadyInProgress | userBlocked | networkFailure | internalError | timeout | csrfMismatch)
PersonnummerConfirmationArgs (userId, name, personnummer)
performance requirements
Deep-link or WebView intercept to state update must complete within 100 ms
Token exchange must have a 30 s HTTP timeout with one automatic retry on transient network errors
Cancel action must call server-side cancellation and pop the route within 500 ms
security requirements
State parameter CSRF validation is mandatory before token exchange — abort and show csrfMismatch error if validation fails
BankID authorization code must be exchanged exactly once; store an 'exchanged' flag in the notifier to prevent replay
Personnummer received from BankID must be stored in flutter_secure_storage and never logged
Server-side BankID session must be explicitly cancelled via API when the user taps Cancel, not just ignored client-side
Error message copy must not expose internal BankID error codes to the user
ui components
Error state container with icon, mapped error message, Retry button, Cancel button
Semantics liveRegion on error message for automatic screen reader announcement
Cancel button (text button, destructive style) visible during awaitingCallback state
Confirmation navigation logic consuming BankIDAuthState.success

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Map BankID error codes in a dedicated bankid_error_mapper.dart file that converts raw error strings to BankIDError enum values and then to user-facing message strings. This separation makes it easy to add new codes or update copy without touching the notifier or UI logic. The Cancel button must be wired to an async action in the notifier: (1) set state to cancelling (disables button to prevent double-tap), (2) await BankIDAuthService.cancelSession(), (3) emit idle, then (4) signal the router to pop. Use ref.listen in the screen widget to respond to BankIDAuthState.success by navigating — keep navigation out of the notifier.

For the WebView path, implement a NavigationDelegate.onNavigationRequest that detects the redirect URI prefix and hands the URL to BankIDAuthService for parsing rather than allowing the WebView to navigate to it. This prevents the BankID redirect from loading in the WebView and instead routes it through the same callback handler used by deep links. This task is high complexity because BankID has many edge-case error codes and the server-side session cancellation adds async complexity to what appears to be a simple Cancel button — plan thorough testing.

Testing Requirements

Unit tests for BankIDAuthService: (1) valid callback with correct state → token exchange called, success emitted with personnummer; (2) callback with wrong state → csrfMismatch error emitted, no token exchange; (3) each documented BankID error code maps to the correct BankIDError enum value; (4) cancelSession() calls the BankID API abort endpoint. Widget tests: (1) simulate success callback → assert navigation to PersonnummerConfirmation with correct args; (2) simulate each error type → assert correct message string is displayed; (3) tap Cancel during awaitingCallback → assert cancelSession called and route popped; (4) tap Retry after error → assert state resets to idle; (5) assert error container has Semantics liveRegion true. Security tests: assert personnummer is not present in any captured log output during a success callback simulation. E2E manual test on device: complete a full BankID flow and verify navigation to Personnummer Confirmation; deliberately cancel mid-flow and verify return to method selector.

Component
BankID Authentication Screen
ui high
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.