critical priority low complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

The bottom sheet from task-009 now contains a GDPR disclosure section above the acknowledgment action, stating: what data is shared (national ID number), with whom (the specific partner organization name, resolved from org context), and for what purpose (identity verification and membership linkage)
The disclosure section is scrollable if the text exceeds visible height; a ScrollController detects when the user has reached the bottom (scrollPosition >= maxScrollExtent - 10px threshold)
The acknowledgment action (checkbox/confirm button) remains disabled until EITHER the user has scrolled to the bottom of the disclosure text OR the user has tapped a clearly labeled 'I have read the disclosure' toggle button
A 'Privacy Policy' hyperlink is present below the disclosure text; tapping it opens the organization's privacy policy URL in an in-app browser (webview_flutter or url_launcher with LaunchMode.inAppWebView)
The privacy policy URL is sourced from the organization context (not hardcoded) so it resolves correctly for NHF, Blindeforbundet, HLF, etc.
Closing the in-app browser returns the user to the bottom sheet without resetting the scroll-read state
Accessibility: the disclosure text container has a Semantics scrollableDescription; the 'Read disclosure' toggle has a clear Semantics label; the privacy policy link announces as 'Opens privacy policy in browser'
If the disclosure text fits entirely within the visible area without scrolling, the acknowledgment is enabled immediately without requiring the toggle
All disclosure copy is sourced from a localizable string resource — no hardcoded UI strings

Technical Requirements

frameworks
Flutter
BLoC
url_launcher or webview_flutter
apis
Organization privacy policy URL (from org context/Supabase org record)
url_launcher LaunchMode.inAppWebView
data models
OrganizationProfile (privacyPolicyUrl, orgName)
AuthSessionState
performance requirements
Privacy policy webview must open within 500ms of tap — use url_launcher canLaunchUrl check before attempting
Scroll detection must not rebuild the entire bottom sheet on every scroll event — use a ValueNotifier<bool> hasReadDisclosure to minimize rebuilds
security requirements
Privacy policy URL must be fetched from the org record in Supabase, not embedded in the app binary, to allow updates without app releases
The in-app browser must not allow navigation away from the privacy policy domain — use NavigationDelegate to block external redirects if using webview_flutter
GDPR disclosure text must be stored server-side or as a localizable asset, not hardcoded, to allow legal updates without a new release
ui components
GdprDisclosureSection — scrollable text container with ScrollController
ReadDisclosureToggle — fallback 'I have read' button for short-text case
PrivacyPolicyLink — InkWell-wrapped text with underline and external link icon
InAppBrowserLauncher — abstraction over url_launcher/webview_flutter

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Add a ScrollController to the disclosure SingleChildScrollView. In the controller's listener, when offset >= maxScrollExtent - 10, set a ValueNotifier hasReadDisclosure to true. Also provide a 'I have read the disclosure' TextButton that sets the same notifier, visible only when the text does not overflow (check in LayoutBuilder). The acknowledgment action from task-009 should now be gated on both the task-009 checkbox AND this notifier via a ValueListenableBuilder.

Pull the org's privacyPolicyUrl from the BLoC state (it should be part of OrganizationProfile already loaded during org selection). For the in-app browser prefer url_launcher with LaunchMode.inAppWebView as it requires no additional package if url_launcher is already a dependency; fall back to webview_flutter only if custom navigation blocking is required. Disclosure copy should live in the ARB/l10n files with placeholders for orgName and dataController so legal can update without code changes.

Testing Requirements

Widget tests: verify acknowledgment button is disabled on render when text overflows; verify it becomes enabled after simulated scroll-to-bottom (manually invoke ScrollController.jumpTo(maxScrollExtent)); verify it becomes enabled after tapping the 'Read disclosure' toggle. Verify the privacy policy link calls the launch function with the correct org URL. Unit test the scroll-threshold detection logic as an isolated function. Mock the OrganizationProfileRepository to return a test org with a known privacyPolicyUrl.

Verify the org name appears correctly in the disclosure text. No e2e test required beyond task-012 coverage.

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.