high priority low complexity frontend pending frontend specialist Tier 7

Acceptance Criteria

On screen mount, OrgSelectionService.getPersistedOrganizationId() is called and its result used to determine pre-selection
If a persisted ID is found and matches an org in the fetched list, that OrgCard renders in its selected visual state immediately (no user tap required)
The CTA button is enabled on mount when a previously persisted selection is found
If the pre-selected card is outside the initial visible viewport, the ListView automatically scrolls to bring it into view after the list renders
If the persisted ID does not match any org in the current list (org removed, ID stale), no card is pre-selected and no error is shown
If no persisted ID exists (first visit), no card is pre-selected and the CTA button is disabled
Pre-selection state and manual tap selection are unified — tapping a different card after pre-selection correctly updates the selection
Scroll-to-selected uses a ScrollController and animates smoothly (not a hard jump) with a short duration (~300ms)

Technical Requirements

frameworks
Flutter
Riverpod
performance requirements
Persisted ID read is synchronous from local storage — no async delay before initial render
Scroll animation must complete within 300ms to avoid disorienting users with cognitive challenges
security requirements
Persisted organization ID stored using flutter_secure_storage (iOS Keychain / Android Keystore), not plain SharedPreferences
ui components
ScrollController for programmatic scroll-to-selected
OrgCard (002-org-card) selected visual state variant
AppButton (primary, enabled when pre-selection loaded)

Execution Context

Execution Tier
Tier 7

Tier 7 - 84 tasks

Can start after Tier 6 completes

Implementation Notes

Read the persisted ID synchronously in initState (or the Riverpod equivalent) before the first build, and pass it as initial state for `_selectedOrg`. After the org list loads, call `WidgetsBinding.instance.addPostFrameCallback` to trigger scroll to the pre-selected item — this ensures the ListView is fully laid out before scrolling. Use `Scrollable.ensureVisible()` on the GlobalKey of the pre-selected OrgCard, or calculate the index offset manually with `ScrollController.animateTo()`. Use flutter_secure_storage for reading the persisted ID — never plain SharedPreferences — per the project security requirements.

Handle the edge case where the org list loads after initState completes by watching the AsyncValue and triggering scroll in the data branch of `.when()`.

Testing Requirements

Widget tests: Test 1: provide persisted ID matching org index 2 in a 3-item list, assert OrgCard at index 2 renders in selected state on mount without any tap. Test 2: provide persisted ID that matches no org in the list, assert zero cards in selected state. Test 3: provide no persisted ID (null), assert zero cards selected and CTA disabled. Test 4: provide persisted ID for a card outside viewport (list of 20 items, pre-selected at index 15), assert ScrollController.offset > 0 after mount (scroll occurred).

Test 5: pre-select via persisted ID, then tap a different card, assert selection updates to tapped card. Unit test: OrgSelectionService.getPersistedOrganizationId() returns correct stored value via mocked flutter_secure_storage.

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.