critical priority medium complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

When ReceiptThresholdState transitions from optional → required, SemanticsService.announce() is called with assertive TextDirection and a message such as 'Receipt is now required for this expense'
When ReceiptThresholdState transitions from required → optional, SemanticsService.announce() is called with polite TextDirection and a message such as 'Receipt is no longer required'
Announcements are NOT triggered on initial widget mount — only on state transitions after the first emission
Announcements are NOT triggered if the state value does not change (e.g., two consecutive 'required' emissions produce only one announcement)
SemanticsService.announce() is called from a side-effect hook (ref.listen or StatefulWidget didUpdateWidget), NOT from the widget build method
Announcement strings are localized and retrievable from the app's existing l10n system
On a physical iOS device with VoiceOver enabled, the assertive announcement interrupts current speech and reads the required message immediately
On a physical Android device with TalkBack enabled, the polite announcement queues after current speech and reads the optional message without interrupting
Widget test confirms SemanticsService.announce is called with correct arguments on state transition using a mock/spy

Technical Requirements

frameworks
Flutter
Riverpod
performance requirements
Announcement side effect must not delay the visual state update — execute in parallel, not sequentially
Debounce rapid threshold crossings to prevent announcement spam (minimum 500ms between repeated identical announcements)
security requirements
Announcement text must not include any financial amount or PII in the spoken string to avoid exposing sensitive data via audio in public settings
ui components
SemanticsService.announce() integration
ref.listen hook in Riverpod consumer
l10n message keys for threshold announcements

Execution Context

Execution Tier
Tier 2

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.

Component
Receipt Attachment Indicator
ui low
Epic Risks (2)
medium impact medium prob technical

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.

medium impact low prob integration

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.