high priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

Tapping 'Open biometric settings' on iOS launches the Settings app at the Face ID & Passcode / Touch ID & Passcode page using the correct iOS URI
Tapping 'Open biometric settings' on Android launches the system biometric enrolment settings using the correct Android URI
If url_launcher canLaunchUrl returns false (URI unsupported on device), the 'Open settings' button is hidden and replaced with a plain-text instruction to navigate manually
A second action button labelled 'Log in with username and password' (terminology-resolved) navigates to the credential login screen via the supplied onNavigateToLogin callback
Both buttons have Semantics labels explicitly set — screen reader reads the full action intent, not just a generic label
Both buttons meet the 44×44px minimum touch target requirement
Neither button is shown when the device has no biometric hardware absent (hardwareAbsent reason hides settings link; notEnrolled shows it)
Credential login button is always visible regardless of reason
url_launcher errors are caught and surfaced as a snackbar, not a crash

Technical Requirements

frameworks
Flutter
url_launcher package
flutter_test
apis
url_launcher (launchUrl, canLaunchUrl)
iOS settings URI: app-settings: or prefs:root=TOUCHID_PASSCODE
Android settings URI: android.settings.BIOMETRIC_ENROLL intent via android-app scheme
data models
BiometricUnavailableReason (enum)
performance requirements
canLaunchUrl check must be performed before render, not on button press, to avoid UI flicker
security requirements
Do not pass sensitive data (tokens, credentials) as query parameters in the settings deep-link URI
Validate the URI format before calling launchUrl to prevent URI injection
ui components
AppButton (primary style for 'Open settings', secondary/ghost style for credential login)
Semantics widget for both action buttons
SnackBar for url_launcher error feedback

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Perform the canLaunchUrl check asynchronously during widget init (or in the parent BLoC) and pass a bool prop showSettingsLink to the banner to keep build() synchronous. Abstract url_launcher behind an interface (e.g. UrlLauncherService) so widget tests can inject a mock. iOS URI options in order of reliability: (1) App-prefs:root=TOUCHID_PASSCODE (works iOS 15+), (2) x-apple-prefs:root=TOUCHID_PASSCODE, (3) app-settings: (opens Settings root).

Test all three on real devices. For Android, use intent:#Intent;action=android.settings.BIOMETRIC_ENROLL;end encoded as a URI. Wrap launchUrl in a try/catch and show a ScaffoldMessenger snackbar on failure. Reference the terminology system for all button labels.

Testing Requirements

Mock url_launcher via a platform channel mock or a wrapper interface. Write widget tests: (1) when canLaunchUrl returns true and reason is notEnrolled, assert 'Open settings' button is visible and tapping it calls launchUrl with the correct platform URI; (2) when canLaunchUrl returns false, assert button is hidden; (3) assert credential login button is always present; (4) assert both buttons have correct Semantics labels. Write integration tests on a physical iOS device (TestFlight build) verifying that tapping the settings deep link actually opens the correct OS screen.

Component
Biometric Unavailable Banner
ui low
Epic Risks (3)
medium impact high prob technical

Flutter's AppLifecycleState.resumed event fires in scenarios beyond simple user-initiated app-switching: it also fires when the native biometric dialog itself dismisses and returns control to Flutter, when system alerts (low battery, notifications) temporarily cover the app, and when the app is foregrounded by a deep link. Without careful debouncing and state tracking, SessionResumeManager can trigger multiple overlapping biometric prompts or prompt immediately after a just-completed authentication, creating a confusing loop.

Mitigation & Contingency

Mitigation: Implement a state machine inside SessionResumeManager with explicit states (idle, prompting, authenticated, awaiting-fallback) and guard all prompt triggers with a state check. Add a minimum inter-prompt interval of 3 seconds. Write widget tests that simulate rapid lifecycle event sequences and verify only one prompt is shown.

Contingency: If the state machine approach proves difficult to test or maintain, replace it with a simple boolean isPromptActive flag with a debounce timer, accepting slightly less precise semantics in exchange for simpler reasoning about concurrent lifecycle events.

high impact medium prob technical

The BiometricPromptOverlay appears on top of the existing app content when the app resumes. If focus is not correctly transferred to the overlay and returned to the underlying screen after authentication, screen reader users (VoiceOver/TalkBack) will either be unable to interact with the prompt or will lose their navigation position in the app after authentication completes, violating WCAG 2.2 focus management requirements and creating a broken experience for Blindeforbundet users.

Mitigation & Contingency

Mitigation: Use Flutter's FocusScope and FocusTrap utilities to capture focus within the overlay on presentation and restore the previous FocusNode after dismissal. Add a live region announcement (using the accessibility live region announcer component) when the overlay appears. Include dedicated VoiceOver and TalkBack test cases in the acceptance criteria.

Contingency: If FocusTrap behaviour proves unreliable across Flutter versions, implement the overlay as a full Navigator push to a modal route rather than an overlay widget, which gives Flutter's built-in modal semantics and focus management automatic WCAG-compliant behaviour.

low impact high prob technical

The BiometricUnavailableBanner needs to deep-link to biometric enrollment settings on both iOS and Android. iOS uses a single URL scheme (app-settings:) that opens the app's settings page. Android has no universal URL for biometric settings — the correct Intent action (Settings.ACTION_BIOMETRIC_ENROLL) was introduced in API 30, with different fallback actions required for API 23-29. Using the wrong action on older Android devices either crashes or navigates to an unrelated settings screen.

Mitigation & Contingency

Mitigation: Build an Android API version check into LocalAuthIntegration that selects the correct Intent action based on the runtime SDK version. Test against Android API 23, 28, and 30+ in the CI matrix. For iOS, validate that the app-settings: URL scheme is correctly declared.

Contingency: If the Android settings Intent fragmentation cannot be resolved reliably for all target API levels, fall back to navigating to the top-level Settings screen (Settings.ACTION_SETTINGS) with an overlay instruction telling the user to navigate to 'Security > Biometrics' manually, ensuring the user always has a path to resolve the issue even if the deep link is imprecise.