critical priority low complexity frontend pending frontend specialist Tier 8

Acceptance Criteria

Tapping the CTA button calls OrgSelectionService.selectOrganization(selectedOrg.id) exactly once
During the async persist call, the CTA button displays a CircularProgressIndicator (replacing button label) and is non-interactive
All OrgCards are also non-interactive during the persist operation (prevent double-tap selection change mid-flight)
Navigation to the login screen occurs only after the persist call returns successfully (no optimistic navigation)
If the persist call fails, the button returns to its normal enabled state with the label restored, and a brief error snackbar is shown
The error snackbar message is plain-language (e.g. 'Noe gikk galt. PrΓΈv igjen.') β€” no technical error details exposed
Navigation uses the app router (GoRouter) β€” no direct Navigator.push inside the widget
After navigation, the OrgSelectionScreen is not kept in the widget tree (replaced, not pushed) to prevent back-navigation to org selection after login
No double navigation can occur even if the user taps rapidly before the loading state renders

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase Auth (session context set after org selection)
flutter_secure_storage (via OrgPersistenceRepository)
performance requirements
Persist operation must complete within 1 second (local secure storage write is near-instant)
Navigation transition must be smooth β€” no visible jank during GoRouter replacement
security requirements
Organization ID persisted to flutter_secure_storage (iOS Keychain / Android Keystore) β€” never plain SharedPreferences
No organization PII (name, metadata) stored in persist β€” only the UUID
Navigation guard on the login route verifies org selection is persisted before rendering
ui components
AppButton with loading state variant (CircularProgressIndicator replacing label)
SnackBar for persist failure feedback
GoRouter for navigation (context.go or context.pushReplacement to login route)

Execution Context

Execution Tier
Tier 8

Tier 8 - 48 tasks

Can start after Tier 7 completes

Implementation Notes

Use a local `bool _isLoading` state variable to guard the button and prevent re-entry. Set `_isLoading = true` before the await, reset to false in the catch block. In the try block, do NOT reset to false β€” navigate away instead. Use `context.go('/login')` (GoRouter replacement) rather than `context.push` to ensure back navigation returns to org selection's parent, not org selection itself.

The `if (!mounted) return` check after await is required in Flutter to prevent setState on a disposed widget. The loading indicator on the button should replace the label text, not be appended β€” this signals clearly to users with cognitive challenges that an action is in progress.

Testing Requirements

Widget tests: Test 1: mock OrgSelectionService.selectOrganization to complete successfully, tap CTA, assert navigation to login route was triggered (mock GoRouter or capture navigation events). Test 2: during persist (delayed mock), assert button shows loading state and OrgCards are non-interactive. Test 3: mock persist throwing an exception, tap CTA, assert no navigation occurs, assert SnackBar appears with error text. Test 4: rapidly tap CTA twice, assert selectOrganization called only once (debounce or loading guard).

Test 5: after successful persist, assert OrgSelectionScreen is replaced (not pushed) β€” back button does not return to org selection. Integration test: verify persisted org ID is readable from flutter_secure_storage after successful CTA tap.

Epic Risks (3)
high impact medium prob technical

Flutter's Semantics system requires explicit configuration for custom card widgets β€” the default widget tree semantics may merge child nodes incorrectly, causing VoiceOver to announce partial or garbled text for each OrgCard. This is a critical failure for Blindeforbundet and violates the contractual WCAG 2.2 AA requirement.

Mitigation & Contingency

Mitigation: Wrap each OrgCard in a Semantics widget with an explicit label combining organization name and role label, set button role, and excludeSemantics: true on all child widgets to prevent double-announcement. Test on a physical iOS device with VoiceOver enabled before marking the epic complete.

Contingency: If automated semantics are insufficient, implement a dedicated AccessibilityLabel builder in the OrgCard that constructs the announcement string from OrgLabelsProvider output and passes it directly to the Semantics label field.

medium impact medium prob technical

Organization logos must load before the screen is considered ready. If assets are fetched from a remote URL and network latency is high (a realistic scenario for rural NHF users), the screen may render with broken image placeholders, degrading the first-impression quality and potentially confusing screen reader users whose VoiceOver announcements would reference an empty image.

Mitigation & Contingency

Mitigation: Cache organization logos locally after first load using the org-branding-cache pattern. For the initial load, render a styled placeholder with the organization's initials and primary color while the logo loads asynchronously. Ensure the Semantics label is derived from the organization name, not the image asset, so screen reader announcements are never dependent on image load state.

Contingency: If caching implementation is out of scope for this epic, ship with initials-based placeholders as permanent fallbacks and defer logo loading to the org-branding-cache epic. The screen remains fully functional and accessible without logos.

medium impact low prob scope

The user story specifies that single-organization users bypass the selection screen and are routed directly into the app. If the bypass logic is implemented in the UI layer rather than in OrgSelectionService, it creates a tight coupling that breaks if the routing logic is ever reused or tested in isolation. It also creates a potential timing issue where the screen briefly renders before the bypass fires.

Mitigation & Contingency

Mitigation: Implement the single-org bypass entirely in OrgSelectionService.getPersistedSelection() or as a route guard β€” the screen should never be pushed onto the navigation stack for single-org users. The OrgSelectionScreen widget itself has no knowledge of this bypass.

Contingency: If the route guard approach is deferred to the routing epic, add a local guard in the OrgSelectionScreen's initState that calls OrgSelectionService, auto-selects if count is 1, and navigates forward before the first frame is painted, using a loading indicator to prevent the flash.