high priority low complexity frontend pending frontend specialist Tier 0

Acceptance Criteria

Widget accepts two inputs: Stream<bool> isRequired and bool isAttached
State 1 — optional-not-attached (isRequired emits false, isAttached is false): renders a neutral grey info row with icon (Icons.info_outline) and label 'Receipt: optional'
State 2 — required-not-attached (isRequired emits true, isAttached is false): renders a warning-toned row with icon (Icons.warning_amber_rounded or similar) and label 'Receipt: required' using the design token warning color
State 3 — attached (isAttached is true, regardless of isRequired): renders a success-toned row with icon (Icons.check_circle_outline) and label 'Receipt: attached' using the design token success/confirmation color
Visual state transitions between states are smooth — use AnimatedSwitcher with 150ms duration, respecting MediaQuery.disableAnimations
Widget uses only design token values for colors, typography, and spacing
Widget renders correctly within a form field row at 320dp screen width without overflow
Widget is a StatelessWidget with a StreamBuilder internally for the isRequired stream — no external state management dependency in this task
When isRequired stream has not yet emitted (ConnectionState.waiting), widget renders the optional-not-attached state as default
Widget test covers all three distinct visual states and the loading/waiting initial state

Technical Requirements

frameworks
Flutter
Dart
data models
ReceiptThreshold
OrgConfig (threshold value source)
performance requirements
StreamBuilder must not cause parent form widget to rebuild — scope the rebuild to the indicator row only
Widget height must be fixed (e.g., 40dp) to prevent form layout shifts when state changes
security requirements
Widget must not read threshold values directly from OrgConfig — it only consumes the pre-resolved boolean stream provided by the caller
ui components
Row with Icon + Text
AnimatedSwitcher for state transitions
Design token color references (neutral, warning, success)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

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(state).

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: (1) initial waiting state renders optional appearance, (2) stream emits false + isAttached false → optional-not-attached state, (3) stream emits true + isAttached false → required-not-attached state with warning styling, (4) isAttached true → attached state regardless of stream value, (5) AnimatedSwitcher child key changes on state transition. Assert icon types and text labels in each state. Verify fixed-height constraint prevents layout shift by checking widget size before and after state transition.

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.