critical priority low complexity frontend pending frontend specialist Tier 3

Acceptance Criteria

The widget is presented as a DraggableScrollableSheet or showModalBottomSheet immediately after BankID or Vipps resolves and returns the personnummer
The personnummer is displayed in masked format showing exactly the first 6 digits followed by 5 asterisks (e.g., '123456*****') — the raw value is never rendered in the widget tree
An acknowledgment mechanism (checkbox with label 'I confirm this is my national ID' OR a single confirm button) is present and the proceed action is disabled until the user activates it
Tapping outside the modal or pressing the device back button does NOT dismiss the modal — the user must either confirm or explicitly cancel via a labeled 'Cancel' action that routes back to the method selector
On confirmation, the widget emits a PersonnummerConfirmed event via BLoC and the caller navigates to the home screen
On cancellation, the session is invalidated and the user is routed back to the auth method selector with appropriate state cleanup
The masked personnummer string is generated by a pure utility function (maskPersonnummer) that is unit tested independently
The widget accepts personnummer as a constructor parameter — it does not fetch it internally, keeping it stateless with respect to data fetching
Semantics label on the masked display reads 'Your national ID number, partially hidden for security' for screen readers

Technical Requirements

frameworks
Flutter
BLoC
apis
BankID callback data (personnummer field)
Vipps callback data (personnummer field)
data models
AuthSessionState
UserIdentity (personnummer, source: bankid|vipps)
performance requirements
Modal must appear within one frame after the auth callback resolves — no async gap between callback and modal display
Widget rebuild must not re-fetch personnummer — pass it via constructor at the time the modal is opened
security requirements
The raw personnummer must never be stored in widget state, only the masked display string
The raw personnummer is passed once from the auth callback to the BLoC event and immediately discarded from UI layer
Do not log or print the personnummer anywhere in debug or release builds
The confirmation event sent to the backend must use the session token, not the personnummer string, to avoid re-transmission
ui components
PersonnummerConfirmationBottomSheet — modal container with drag handle
MaskedPersonnummerDisplay — styled text widget with security icon
AcknowledgmentCheckbox or ConfirmButton — disabled until user interacts
CancelAction — text button for explicit dismissal

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement maskPersonnummer as a top-level or static utility: take the 11-digit string, return '${input.substring(0, 6)}*****'. Bottom sheet should use showModalBottomSheet with isDismissible: false and enableDrag: false to prevent accidental dismissal. Use a StatefulWidget inside the sheet only for the checkbox toggle state — the confirmation logic itself should go through the BLoC. If using a confirm button instead of a checkbox, use a design-token colored AppButton that transitions from disabled to enabled styling when tapped once.

Ensure the modal is added to the Navigator via the root navigator (useRootNavigator: true) so it appears above any nested navigators in the shell route structure. Keep this widget GDPR-neutral — do not display who owns the data or for what purpose here; that is handled in task-010.

Testing Requirements

Unit test the maskPersonnummer() utility: verify '12345678901' returns '123456*****', verify short/invalid input returns a safe fallback without crashing. Widget tests: verify the proceed button/checkbox is disabled on initial render; verify it becomes enabled after acknowledgment interaction; verify tapping cancel triggers navigation pop to method selector; verify the masked string is displayed and not the raw value. Use a mock BLoC to verify PersonnummerConfirmed event is dispatched on confirmation. No integration test needed beyond what task-012 covers.

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.