critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

A private Supabase Storage bucket named receipt-images exists with public access disabled and bucket-level RLS requiring authenticated users
uploadReceiptImage(Uint8List bytes, String fileName, {void Function(double progress)? onProgress}) uploads the file to the bucket under path {uid}/{receiptId}/{fileName}, calls onProgress with values in [0.0, 1.0] during upload, and returns the storage path string on completion
getSignedUrl(String storagePath, {Duration expiry = const Duration(hours: 1)}) returns a signed URL valid for the specified duration; throws ReceiptStorageException if the path does not exist
deleteReceiptImage(String storagePath) removes the file from storage; is idempotent (does not throw if file is already absent)
Retry logic: transient network errors (SocketException, TimeoutException) trigger up to 3 retries with exponential backoff (1s, 2s, 4s); non-retryable errors (4xx from storage) are thrown immediately
File size is validated client-side before upload: maximum 10 MB; files exceeding this limit throw a ValidationException without making a network call
Accepted MIME types are image/jpeg and image/png only; other types throw a ValidationException
Progress callback is invoked on the UI isolate (not a background isolate) so it can be connected directly to a Flutter progress indicator
All three methods are covered by unit tests with a mocked Supabase storage client

Technical Requirements

frameworks
Flutter
Supabase Dart client (supabase_flutter)
BLoC
apis
Supabase Storage API (upload, createSignedUrl, remove)
dart:io for file size/type checks
data models
Receipt
ReceiptStoragePath
performance requirements
Upload progress must update at minimum every 100KB transferred
Signed URL generation must complete in under 300ms
Upload of a 5MB image must complete in under 10 seconds on a standard LTE connection
security requirements
Storage paths must be scoped to the authenticated user's UID as the first path segment to prevent path traversal
Signed URLs must have a maximum expiry of 1 hour; do not generate permanent public URLs
MIME type validation must be performed on file header bytes (magic bytes), not solely on file extension, to prevent extension spoofing
Bucket must be configured as private (not public) in Supabase dashboard

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The Supabase Dart client's storage upload does not natively stream progress — implement progress by splitting large files into chunks using Uint8List.sublist() and uploading via multipart, or use the uploadBinary method with a custom HttpClient wrapper that intercepts sent bytes. A simpler approach for the initial implementation: use the standard upload call and emit progress as 0.0 (start) and 1.0 (complete) only, with a TODO comment for streaming progress in a future iteration. For MIME type magic byte detection: JPEG starts with bytes [0xFF, 0xD8, 0xFF]; PNG starts with [0x89, 0x50, 0x4E, 0x47]. Implement a private _detectMimeType(Uint8List bytes) method checking the first 4 bytes.

For retry logic, implement a generic _withRetry(Future Function() operation, {int maxAttempts = 3}) helper using Future.delayed with exponential backoff. Store bucket name and max expiry as constants rather than magic strings.

Testing Requirements

Write unit tests with a MockSupabaseStorageClient using mocktail: (1) uploadReceiptImage triggers onProgress callbacks at expected intervals and returns the storage path, (2) uploadReceiptImage throws ValidationException for files > 10MB without making a network call, (3) uploadReceiptImage throws ValidationException for non-image MIME types, (4) getSignedUrl returns a URL string on success and throws ReceiptStorageException on storage 404, (5) deleteReceiptImage does not throw when the file is absent (idempotent), (6) retry logic: mock the upload to throw SocketException twice then succeed on third attempt; assert the method returns successfully and the progress callback was called. Write one integration test against a local Supabase storage instance verifying end-to-end upload + signed URL retrieval.

Component
Receipt Attachment Service
service high
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.