critical priority low complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

OrgSelectionScreen is a StatefulWidget (or ConsumerStatefulWidget for Riverpod) placed at its correct route in the app router
Screen layout top-to-bottom: app logo/name → scrollable OrgCard list → sticky primary CTA button
The primary CTA button is disabled (non-interactive, visually dimmed per design tokens) when no organization is selected
The primary CTA button becomes enabled immediately upon selecting any OrgCard
Screen contains exactly one primary action button — no secondary buttons, back arrows, or alternate CTAs visible on this screen
ListView is scrollable when the organization list exceeds the visible viewport; short lists do not show unnecessary scroll indicators
App logo/name area uses design token values for sizing and typography — no hardcoded pixel values
OrgSelectionScreen renders correctly on both small (iPhone SE 375px wide) and large (iPad 1024px wide) screen sizes
Screen does not use a Scaffold AppBar — the logo/name area serves as the visual header per single-action layout pattern
Widget tree is shallow and performant — no unnecessary nesting of Column inside Column

Technical Requirements

frameworks
Flutter
Riverpod
performance requirements
Screen must render first frame within 300ms on mid-range devices
ListView must use itemExtent or SliverFixedExtentList if card heights are uniform to reduce layout cost
security requirements
No organization metadata is hardcoded into the screen widget — all data comes from service layer
ui components
OrgCard widget (002-org-card)
AppButton (primary variant, disabled state)
ListView.builder for organization list
SafeArea wrapper for notch/home indicator avoidance
Column with MainAxisAlignment.spaceBetween for top/center/bottom layout sections

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Manage the selected organization in local StatefulWidget state (`Organization? _selectedOrg`) — do not put transient UI selection state in Riverpod global state. Riverpod is used only to inject the OrgSelectionService. Use a Column with `Expanded` around the ListView to push the CTA button to the bottom without needing a Stack.

Wrap the entire Column in a SafeArea. For the CTA button, bind `onPressed: _selectedOrg != null ? _handleConfirm : null` — Flutter's ElevatedButton/AppButton renders as disabled automatically when onPressed is null, no custom disabled logic needed. The single-action layout rule from the workshop documentation (cognitive accessibility for NHF's stroke survivors) is critical: resist any temptation to add a 'skip' or 'back' link on this screen.

Testing Requirements

Widget tests with flutter_test: Test 1: render screen with empty org list, assert CTA button is disabled. Test 2: render screen with 3 mock orgs, assert 3 OrgCard widgets present. Test 3: tap first OrgCard, assert CTA button transitions to enabled state. Test 4: render on 375px width and 1024px width — assert no overflow RenderFlex errors.

Test 5: assert exactly one button widget with primary style is present (no secondary actions). Golden test: capture screenshot of screen with pre-selected org for visual regression baseline.

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.