high priority low complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

pickFromCamera() returns a ReceiptImageFile value object containing the XFile reference, MIME type, and file size on success
pickFromGallery() returns a ReceiptImageFile value object on success with identical structure to camera capture
Both methods return null (not throw) when the user cancels the picker without selecting an image
Permission denial on iOS (camera or photo library) results in a typed PermissionDeniedFailure being returned, not an unhandled exception
Permission denial on Android results in the same PermissionDeniedFailure type
ReceiptImageFile value object is immutable and implements equality by file path
Adapter is injectable via dependency injection (Riverpod provider or BLoC service locator) and replaceable with a mock in tests
No direct import of image_picker exists outside this adapter class
All public methods are covered by unit tests using a mock ImagePicker
WCAG 2.2 AA: any error states exposed upward include a human-readable description string suitable for screen reader announcement

Technical Requirements

frameworks
Flutter
Riverpod
image_picker (Flutter plugin)
data models
ReceiptImageFile (value object: XFile path, mimeType, fileSizeBytes)
PermissionDeniedFailure (sealed failure type)
performance requirements
Picker must open within 300 ms of user tap on both iOS and Android
No memory leaks from unclosed file handles — XFile must not be held beyond the method return
security requirements
Only camera and photo-library permissions are requested — no broader storage permission on Android 13+
File path stored in ReceiptImageFile must not be logged or persisted as plain text

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Create an abstract interface (e.g., IReceiptImagePicker) alongside the concrete ReceiptImagePickerIntegration class so unit tests can inject a fake. Use Either or a sealed Result type rather than nullable returns mixed with exceptions — this forces callers to handle both error and cancellation explicitly. On Android, call permission_handler or rely on image_picker's built-in permission handling consistently; do not mix both. On iOS, ensure NSCameraUsageDescription and NSPhotoLibraryUsageDescription keys are already present in Info.plist (verify, do not add if present).

The ReceiptImageFile value object should carry only metadata needed downstream — do not load bytes here; compression happens in a separate service.

Testing Requirements

Unit tests (flutter_test): mock ImagePicker to simulate successful camera pick, successful gallery pick, user cancellation on both paths, and permission denied on both paths. Verify return types and null behavior. Integration smoke test on a physical device or emulator confirming the permission dialog appears and camera sheet opens. No golden tests required for this adapter layer.

Component
Receipt Camera Sheet
ui medium
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.