critical priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Organization Dart class is defined with required fields: id (String), displayName (String), logoUrl (String?), isSelected (bool)
Organization class implements equality (== and hashCode) based on id only
Organization class is immutable (all fields final, uses const constructor where possible)
Organization class includes a copyWith() method to toggle isSelected without mutating the original
OrgCard widget constructor exposes: organization (Organization), onTap (VoidCallback), and an optional key parameter — no additional required parameters
All identifiers follow Dart/Flutter naming conventions (UpperCamelCase for classes, lowerCamelCase for fields)
File is placed at lib/features/org_selection/models/organization.dart or equivalent feature-layer path
No business logic or UI code is in the model file
Model is exported from the feature's barrel file
Code compiles with zero warnings under strict Dart analysis options

Technical Requirements

frameworks
Flutter (Dart)
Dart data class patterns (manual or with equatable package if already in pubspec)
data models
Organization (id, displayName, logoUrl, isSelected)
performance requirements
Model instantiation must be O(1) — no async operations in constructor
security requirements
logoUrl must be nullable — never assume a URL is present
id must be treated as opaque string — no parsing or assumption about format
ui components
OrgCard (consumer of this model)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Check whether the project already uses the equatable package for value equality. If equatable is present, extend Equatable and override props with [id]. If not, manually implement == and hashCode on id alone. Do not add logoUrl to equality — two records representing the same org with different logo CDN URLs should still be equal.

The logoUrl field should be String? (nullable) so OrgCard can show an initials fallback without null-check ceremony. Consider adding a static factory Organization.fromJson(Map) to ease Supabase integration in later tasks, but keep it in this file to centralize deserialization.

Testing Requirements

Write a small unit test file (test/features/org_selection/models/organization_test.dart) with three tests: (1) equality holds when ids match regardless of other fields, (2) copyWith correctly overrides isSelected while preserving all other fields, (3) two Organization instances with different ids are not equal. These tests guard the contract relied upon by later OrgCard and OrgSelectionScreen tasks.

Component
Organization Card Widget
ui low
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.