high priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

Widget accepts a Stream<double>? uploadProgress parameter where values are in the range 0.0–1.0
When uploadProgress stream is active and emitting values between 0.0 and 1.0, a semi-transparent dark overlay covers the thumbnail and a CircularProgressIndicator is shown centered with the numeric percentage visible
When the stream completes normally (done without error), overlay transitions to show a green checkmark icon (Icons.check_circle) for 1.5 seconds then fades out
When the stream closes with an error, overlay shows a retry icon (Icons.refresh) in amber/warning color, indicating upload failure
When uploadProgress is null, no overlay is rendered and the widget behaves identically to task-001 baseline
All overlay entry/exit transitions use AnimatedOpacity or AnimatedSwitcher with a duration of 200ms
Transition animations are suppressed (set to Duration.zero) when MediaQuery.of(context).disableAnimations is true (reduced-motion compliance)
Widget remains a StatelessWidget externally — the StreamBuilder is internal and does not leak stream subscription state to callers
Overlay does not obscure the file metadata row below the thumbnail — overlay is scoped to the image container only
Widget test covers: uploading state at 50%, completion state, error state, and null stream state

Technical Requirements

frameworks
Flutter
Dart
data models
Receipt
UploadProgress (stream type double)
performance requirements
StreamBuilder must not cause unnecessary full widget rebuilds outside the overlay Stack — use a targeted rebuild scope
Percentage text update must not cause layout thrash — use a fixed-width text container
security requirements
Stream must not be stored in widget state beyond the StreamBuilder subscription lifecycle
ui components
CircularProgressIndicator (determinate, value from stream)
AnimatedSwitcher for state transitions
Stack with Positioned overlay
Icons.check_circle, Icons.refresh

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Extend ReceiptThumbnailPreview from task-001 by adding the uploadProgress parameter — do not create a new widget class. Wrap the image container in a Stack and conditionally add an overlay child driven by a StreamBuilder>. Use snapshot.connectionState to distinguish: ConnectionState.active (show progress), ConnectionState.done with no error (show checkmark then fade), ConnectionState.done with error (show retry). For reduced-motion: read MediaQuery.of(context).disableAnimations once in the build method and pass the conditional Duration to AnimatedSwitcher and AnimatedOpacity.

The overlay background should use Colors.black.withOpacity(0.55) — verify this meets 3:1 contrast ratio against the progress indicator color using design tokens.

Testing Requirements

Use flutter_test with a StreamController to simulate upload states. Test cases: (1) null stream shows no overlay, (2) stream emitting 0.5 shows progress indicator at 50%, (3) stream completion triggers checkmark state, (4) stream error triggers retry icon state, (5) MediaQuery.disableAnimations = true results in Duration.zero animation durations. Use pump and pumpAndSettle carefully with fake async for stream timing.

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.