critical priority medium complexity integration pending fullstack developer Tier 5

Acceptance Criteria

Tapping the attach receipt affordance in the expense wizard opens ReceiptCameraSheet as a modal overlay without navigating away from the current wizard step
All form field values (amount, date, activity type, notes, etc.) are preserved exactly after the sheet is dismissed — no field reset occurs
When ReceiptCameraSheet returns an image file, ReceiptAttachmentService.startUpload(file) is called immediately without waiting for the user to submit the form
ReceiptAttachmentIndicator in the form reflects upload progress in real time (idle → uploading → uploaded / failed) without any manual refresh
Form submission is allowed and proceeds normally when ReceiptAttachmentService is in the uploaded state
Form submission is blocked only when ReceiptThresholdValidator determines a receipt is mandatory (amount > 100 kr) AND ReceiptAttachmentService state is idle or failed — an inline validation message explains why
Form submission is allowed when upload is still in progress (uploading state); submission waits for upload to complete via ReceiptAttachmentService before sending the expense claim to the backend
If upload fails after form submission is initiated, the user is notified inline and can retry without losing form data
Wizard step progress indicator remains visible and accurate while the sheet is open
The integration does not introduce any new BLoC events or Riverpod providers beyond those defined in ReceiptAttachmentService and the existing expense wizard state

Technical Requirements

frameworks
Flutter
BLoC or Riverpod
apis
ReceiptAttachmentService (internal)
ReceiptCameraSheet (internal)
ReceiptAttachmentIndicator (internal)
ReceiptThresholdValidator (internal)
Supabase Storage (via service)
performance requirements
Sheet open and close must not cause the expense wizard form to re-render from scratch — use const widgets and stable keys
Upload initiation after image selection must occur within one frame of the sheet returning the file
security requirements
Image file reference is passed only to ReceiptAttachmentService — not stored in wizard state or BLoC events in raw form
Upload occurs over TLS via Supabase Storage SDK
Receipt images stored in private Supabase Storage bucket with RLS — not publicly accessible
ui components
ReceiptCameraSheet (task-009)
ReceiptAttachmentIndicator (task-012)
Inline validation message widget (design system error text)

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

The expense wizard state (BLoC or Riverpod Notifier) should hold a receiptAttachmentServiceId or a reference to the active ReceiptAttachmentService instance so that the ReceiptAttachmentIndicator and the submit logic share the same service instance. Do not reconstruct ReceiptAttachmentService on each build — provide it via dependency injection (Riverpod Provider or BLoC dependency). For the 'wait for upload on submit' behaviour, use await service.uploadCompleter.future inside the form submission handler — ReceiptAttachmentService should expose a Future that resolves when the current upload succeeds or fails. Use showModalBottomSheet with a Future return — the wizard's _onAttachTapped method is async and awaits the sheet result before calling service.startUpload().

Keep the wizard's form key (GlobalKey) stable across rebuilds to prevent state loss.

Testing Requirements

Widget integration tests: verify form fields retain values after ReceiptCameraSheet dismissal; verify ReceiptAttachmentService.startUpload is called when sheet returns a file; verify ReceiptAttachmentIndicator transitions to uploading state immediately after image selection; verify form submit button is disabled with correct error message when receipt is mandatory and not attached; verify form submit is enabled when amount is below 100 kr regardless of attachment; verify wizard submits successfully when upload completes before submission; verify wizard waits for in-progress upload before submitting. Use mocktail for ReceiptAttachmentService and flutter_test pump helpers to simulate async upload completion. Test on both iOS and Android form factor targets.

Component
Receipt Camera Sheet
ui medium
Dependencies (4)
Handle all ReceiptCameraSheet dismissal scenarios: user swipes down, taps backdrop, presses system back button, or taps Cancel. On dismissal without selection return null to the calling widget without triggering upload. Prevent accidental dismissal during active upload with a confirmation dialog. Integrate with WillPopScope for Android back button interception. epic-receipt-capture-and-attachment-core-logic-task-011 Extend ReceiptAttachmentService to perform Supabase Storage uploads in a background isolate, emitting upload progress events via StreamController. The expense form must remain interactive and submittable during upload. Implement upload cancellation, retry on transient network errors (max 3 attempts with exponential backoff), and final completion/failure events. epic-receipt-capture-and-attachment-core-logic-task-008 Implement the ReceiptCameraSheet as a Flutter DraggableScrollableSheet presented as a bottom sheet within the expense wizard. Render two action buttons: 'Take photo' (camera) and 'Choose from library' (gallery). Apply design token styling for dark surface, rounded corners, and touch targets meeting 44pt minimum. Integrate ReceiptImagePickerIntegration for both actions. epic-receipt-capture-and-attachment-core-logic-task-009 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. epic-receipt-capture-and-attachment-core-logic-task-012
Epic Risks (3)
high impact medium prob technical

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.

high impact medium prob scope

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.

medium impact low prob integration

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.