Build ReceiptCameraSheet bottom sheet modal UI
epic-receipt-capture-and-attachment-core-logic-task-009 — 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.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Use showModalBottomSheet with isScrollControlled: true to allow full-screen drag. Wrap content in SafeArea with bottom: true to handle home indicator on iOS. DraggableScrollableController is not needed unless snap behaviour is required — keep it simple. Do not use Navigator.push for this sheet; it must overlay the wizard form without replacing it.
Pass ReceiptImagePickerIntegration as a constructor dependency (not created inside the widget) to enable testability. Design token values must be read from the Theme extension — do not use Theme.of(context).colorScheme directly unless your token system wraps it. Handle the case where image_picker returns null (user cancelled OS picker) by propagating null back up cleanly. Avoid setState inside an async gap — use a local BLoC or Cubit if upload status tracking is needed here (though that belongs to task-012).
Testing Requirements
Widget tests using flutter_test: verify sheet renders with correct button count and labels; verify tapping 'Take photo' invokes ReceiptImagePickerIntegration.pickFromCamera(); verify tapping 'Choose from library' invokes ReceiptImagePickerIntegration.pickFromGallery(); verify sheet can be dismissed without selection; verify touch targets meet 44pt minimum via widget bounds assertions. Use mocktail to mock ReceiptImagePickerIntegration. Golden tests for visual regression of sheet layout on at least two screen sizes. No integration tests required for this task.
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.