Build ReceiptAttachmentIndicator widget with upload state
epic-receipt-capture-and-attachment-core-logic-task-012 — Implement the ReceiptAttachmentIndicator widget displayed in the expense form showing current receipt attachment state: idle (attach button), uploading (progress bar + percentage), uploaded (thumbnail + remove option), or failed (retry button with error message). Connect to ReceiptAttachmentService upload stream and ReceiptThresholdValidator to show mandatory indicator when amount exceeds 100 kr.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Model state as a sealed class or enum: ReceiptAttachmentState { idle, uploading(progress: double), uploaded(signedUrl: String), failed(message: String) }. Use StreamBuilder or a Riverpod StreamProvider / BLoC to consume ReceiptAttachmentService's state stream — do not poll. Wrap the entire widget in AnimatedSwitcher keyed by state type to get smooth cross-fade between states. For the progress percentage, display (progress * 100).round() to avoid float precision display artifacts.
The mandatory indicator should be driven by a parameter passed to the widget (isMandatory: bool) computed from ReceiptThresholdValidator in the parent — do not call the validator inside the widget to keep it pure. Signed URL caching: if the thumbnail URL changes on retry, bust the CachedNetworkImage cache using the URL as key. Ensure the remove action shows a brief loading state on the button itself before ReceiptAttachmentService responds.
Testing Requirements
Widget tests for all four states: verify idle state renders attach button; verify uploading state renders progress bar and percentage text; verify uploaded state renders thumbnail container and remove button; verify failed state renders retry button and error message text; verify mandatory indicator appears when ReceiptThresholdValidator returns true and state is idle; verify mandatory indicator absent when threshold not exceeded. Use mocktail to mock ReceiptAttachmentService stream and ReceiptThresholdValidator. Test AnimatedSwitcher transitions by pumping widget and asserting intermediate states. Golden tests for all four states.
Non-blocking upload creates a race condition: if the claim record is submitted and saved before the upload completes, the storage path may never be written to the claim_receipts table, leaving the claim with a missing receipt that was nonetheless required.
Mitigation & Contingency
Mitigation: Design the attachment service to queue a completion callback that writes the storage path to the claim record upon upload completion, even after the claim form has submitted. Use a local task queue with persistence to survive app backgrounding. Test the race condition explicitly with simulated slow uploads.
Contingency: If the async path association proves unreliable, fall back to blocking upload before claim submission with a clear progress indicator, accepting the UX trade-off in exchange for data integrity.
The offline capture requirement (cache locally, sync when connected) significantly increases state management complexity. If the offline queue is not durable, receipts captured without connectivity may be lost when the app is killed, causing claim submission failures users are not aware of.
Mitigation & Contingency
Mitigation: Persist the offline upload queue to local storage (e.g., Hive or SQLite) on every state transition. Implement background sync using WorkManager (Android) and BGTaskScheduler (iOS). Scope the initial delivery to online-only flow if offline sync cannot be adequately tested before release.
Contingency: Ship without offline support in the first release, displaying a clear 'Upload requires connection' message. Add offline sync as a follow-on task once the core online flow is validated in production.
The inline bottom sheet presentation within a multi-step wizard can conflict with existing modal navigation and back-button handling, particularly if the expense wizard itself uses nested navigation or custom route management.
Mitigation & Contingency
Mitigation: Review the expense wizard navigation architecture before implementation. Use showModalBottomSheet with barrier dismissal disabled to prevent accidental dismissal. Coordinate with the expense wizard team on modal stacking behavior and ensure the camera sheet does not interfere with wizard step transitions.
Contingency: If modal stacking causes navigation issues, present the camera sheet as a full-screen dialog using PageRouteBuilder with a transparent barrier, preserving wizard state via the existing Bloc while still appearing inline.