Add live region announcements on threshold crossing
epic-receipt-capture-and-attachment-ui-accessibility-task-007 — When the threshold stream emits a state change (optional → required or required → optional), emit a Flutter Semantics live region announcement so VoiceOver and TalkBack read the new status aloud without user focus movement. Use SemanticsService.announce() with assertive politeness for the required transition and polite for the optional transition.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Use ref.listen (not ref.watch) inside a ConsumerStatefulWidget to observe ReceiptThresholdState changes and call SemanticsService.announce() as a side effect. Store the previous state in the widget's state object to detect transitions. Import 'package:flutter/semantics.dart' for SemanticsService and TextDirection. For assertive announcements, use assertive: true parameter (Flutter 3.x).
Be aware that SemanticsService.announce() is a no-op when accessibility services are disabled — this is the correct behavior and requires no special handling. Add a note in code that this is intentionally not called during build. Coordinate announcement message strings with UX copy to ensure they are concise and action-oriented (screen readers read the full string).
Testing Requirements
Widget tests (flutter_test): mock SemanticsService, trigger threshold state transitions via StreamProvider override, assert announce() called with correct message and TextDirection for each transition; assert no announcement on initial state; assert no duplicate announcement on same-state re-emission. Manual device testing required: iOS with VoiceOver (assertive interrupts speech), Android with TalkBack (polite queues). Document device test results in audit log for task-010 sign-off.
Flutter's accessibility live region support (SemanticsProperties.liveRegion) has known inconsistencies between iOS VoiceOver and Android TalkBack, and between Flutter versions. Threshold-crossing announcements may fail to fire or double-fire, breaking the accessibility contract for Blindeforbundet users.
Mitigation & Contingency
Mitigation: Test live region announcements on physical devices with VoiceOver and TalkBack enabled from the first iteration. Use the AccessibilityLiveRegionAnnouncer component pattern already established in the project. Verify announcement timing relative to Bloc state emissions to avoid double-fires.
Contingency: If Flutter live regions prove unreliable, implement a platform-channel fallback that calls UIAccessibility.post(notification:) on iOS and AccessibilityManager.sendAccessibilityEvent() on Android directly, bypassing Flutter's abstraction.
The org-configurable threshold must be available at form-render time. If the threshold configuration is fetched asynchronously and not cached, the indicator may briefly show the wrong state (e.g., 'optional' before the threshold loads), confusing users and potentially allowing invalid submissions.
Mitigation & Contingency
Mitigation: Ensure the receipt threshold validator loads and caches the org configuration at app startup or organization selection time, not lazily on form open. Use a loading state in the indicator widget rather than defaulting to 'optional' while configuration is pending.
Contingency: If startup caching is not feasible, treat an unknown threshold as 'receipt required' (fail safe) and surface a clear loading indicator until the configuration resolves, preventing invalid submissions while the config loads.