Build Receipt Attachment Indicator base widget
epic-receipt-capture-and-attachment-ui-accessibility-task-005 — Implement the Receipt Attachment Indicator as an inline form widget. In its default state it shows a neutral info row ('Receipt: optional' or 'Receipt: required') derived from the current threshold stream value. Accept a boolean isRequired stream and an isAttached boolean as inputs. Render distinct visual states for optional-not-attached, required-not-attached, and attached.
Acceptance Criteria
Technical Requirements
Implementation Notes
Create the widget at lib/features/receipts/widgets/receipt_attachment_indicator.dart. The three states map cleanly to a sealed class or enum (AttachmentIndicatorState: optionalEmpty, requiredEmpty, attached) — derive this inside the build method from the stream snapshot and isAttached bool, then switch over it for icon, label, and color selection. Use design tokens: AppColors.textSecondary for optional, AppColors.warning for required, AppColors.success for attached (use whatever the actual token names are in the codebase). The AnimatedSwitcher key must change when the state enum value changes — use ValueKey
Keep the widget height fixed with a SizedBox wrapper to prevent form jitter. This widget will later receive an onTap callback to open the camera sheet (future task) — leave a commented TODO placeholder for that parameter so the API extension point is visible.
Testing Requirements
Write flutter_test widget tests using StreamController
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.