Wire threshold stream to attachment indicator
epic-receipt-capture-and-attachment-ui-accessibility-task-006 — Connect the Receipt Threshold Validator reactive stream to the attachment indicator so it dynamically switches between optional and required states as the expense amount changes. When the threshold is crossed, the indicator must update in real time without a full widget rebuild. Use Riverpod StreamProvider to propagate the threshold state.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Define a ReceiptThresholdState sealed class (optional | required | loading) rather than a raw bool to handle the loading/unavailable config state gracefully. The StreamProvider should combine the expense amount stream with the org config stream using Riverpod's combineLatest equivalent (StreamProvider.family or a custom provider). Avoid placing threshold logic in the widget build method — keep it in a dedicated ThresholdEvaluator service class injected via Riverpod. Use select() on the provider where possible to further narrow rebuilds.
The org config repository must be the single source of truth; add a lint rule or comment guard in the indicator widget explicitly forbidding hardcoded numeric comparisons.
Testing Requirements
Unit tests (flutter_test): StreamProvider emits correct ReceiptThresholdState for amounts below, at, and above threshold; null-safety for loading config state. Widget tests: indicator renders 'optional' and 'required' visual states correctly; verify no parent rebuild using tester.binding.hasScheduledFrame. Integration test: provision two orgs with different thresholds via Supabase test fixtures, fill expense amount field incrementally, assert indicator state transitions at each org's correct threshold. No e2e device test required for this task — covered by task-009.
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.