critical priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

The screen renders the official Vipps logo asset and brand colors per Vipps design guidelines without distortion at all screen sizes
A clearly labeled CTA button ('Sign in with Vipps') is displayed and enabled when the screen loads
Tapping the CTA button immediately disables the button and transitions the UI to a loading/progress indicator within one animation frame (< 16 ms)
The loading state includes an animated progress indicator and supporting text (e.g., 'Opening Vipps...') to communicate what is happening
VippsAuthService.initiateLogin() is called exactly once per tap regardless of how quickly the user taps
The screen invokes the device OS to open the Vipps app or browser via the OAuth redirect URL returned by VippsAuthService
The loading state persists until either a deep-link callback is received or a configurable timeout (default 120 s) elapses
The Riverpod provider managing auth state is consumed via ref.watch/ref.listen so the screen rebuilds reactively on state changes
If the user presses the Android back button during loading, they are returned to the method selector screen and the pending auth attempt is cancelled
The screen is covered by a widget test that mocks VippsAuthService and asserts the loading indicator appears after tap

Technical Requirements

frameworks
Flutter
Riverpod
apis
Vipps OAuth 2.0 login API
url_launcher (Flutter plugin) for deep-link/app-switch launch
data models
VippsAuthState (idle | loading | awaitingCallback | error)
OAuthRedirectParams (state, nonce, redirectUri)
performance requirements
Button-to-loading-state transition must complete within one frame (< 16 ms) to feel instantaneous
OAuth URL generation and launch must complete within 500 ms of tap
No jank during the progress animation; use const constructors and avoid rebuilding parent widgets
security requirements
OAuth state parameter must be a cryptographically random value (dart:math SecureRandom or crypto package) generated fresh for each login attempt to prevent CSRF
Redirect URI must be a registered deep-link scheme, not an http endpoint
Do not log the OAuth state or nonce values to console or analytics
ui components
VippsLogoWidget (SVG asset with correct aspect ratio)
AppButton (primary variant) wired to initiateLogin
CircularProgressIndicator or branded loading animation
Supporting status text widget that updates with loading phase messages
WillPopScope / PopScope for back-button interception during loading

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use a StateNotifierProvider (Riverpod) to hold VippsAuthState so the screen subscribes reactively. The StateNotifier should expose initiateLogin() which: (1) emits loading state, (2) generates CSRF state token, (3) calls url_launcher to open the Vipps OAuth URL, (4) starts a timer for the 120 s timeout. The timeout timer must be cancelled if a deep-link callback arrives first — use a Completer that can be completed by either the deep-link handler or the timer. Handle the edge case where the Vipps app is not installed: url_launcher.canLaunchUrl() should be checked; if false, show a browser-based fallback.

The back-button interception during loading state should call a cancel() method on the notifier that sets state back to idle and cancels the pending timer before popping the route. Do not use Navigator.pop() directly in the widget — route the cancel action through the notifier to ensure state is clean. Use const for the Vipps logo widget and static text to prevent unnecessary rebuilds during the loading animation.

Testing Requirements

Widget tests: mock VippsAuthService provider, tap the CTA button, assert (1) button becomes disabled, (2) CircularProgressIndicator appears, (3) VippsAuthService.initiateLogin() was called exactly once. Widget test: simulate back-button press during loading state, assert screen pops and cancellation method on VippsAuthService is called. Unit tests for VippsAuthService: assert OAuth state parameter is unique across successive calls (generate 100, check no duplicates). Integration test on a simulator: launch to Vipps screen, tap button, assert the device attempts to open the Vipps OAuth URL (check url_launcher invocation via a fake).

Manual testing on physical device: verify Vipps app opens or browser fallback activates; verify loading state is displayed during the handshake wait.

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.