high priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

User can initiate receipt capture via camera or photo gallery through a single bottom-sheet action menu.
Captured image is compressed to ≤ 1 MB before upload using a configurable quality parameter; original image is not stored on device after compression.
An upload progress indicator (LinearProgressIndicator or CircularProgressIndicator) is visible from the moment upload begins until the receipt storage adapter confirms success.
On upload success, the progress indicator is replaced by a success state (thumbnail or checkmark); on failure, an inline error message is shown with a retry affordance.
When the entered expense amount is ≥ ExpenseThresholdConfig.receiptRequiredAbove (default 100 NOK), a mandatory-attachment banner is shown with a prominent visual indicator (e.g. amber warning icon + text).
The mandatory-attachment banner is wrapped in Semantics(liveRegion: true) so screen readers announce it when it appears.
Mandatory-attachment state is communicated to the parent ExpenseFormBloc so the form cannot be submitted without a receipt when above threshold.
Camera and gallery permissions are requested with a contextual rationale message before the system dialog appears.
If the user denies permissions, a non-blocking inline message explains why the feature is unavailable and links to device settings.
Widget handles platform exceptions from image_picker gracefully (user cancel, permission denial, file not found) without crashing.
All interactive elements meet 44×44dp touch target minimum.
Widget behaves identically on iOS 16+ and Android 8+ (API 26+).

Technical Requirements

frameworks
Flutter
image_picker (Flutter plugin)
BLoC (flutter_bloc)
apis
Receipt Storage Adapter API (internal — Supabase Storage bucket)
data models
ReceiptAttachment
ExpenseThresholdConfig
UploadProgressState
performance requirements
Image compression must complete within 3 seconds for images up to 10 MB on mid-range devices.
Upload progress updates must arrive at least every 500 ms to provide smooth indicator animation.
Widget must not block the UI thread during compression — run in an isolate or use compute().
security requirements
Uploaded receipt images must be stored in a Supabase Storage bucket with row-level security (RLS) scoped to the submitting user.
Temporary local copies of the compressed image must be deleted after successful upload.
Image picker must use ImageSource.camera with saveToGallery: false to avoid persisting receipts in the device photo library.
ui components
ReceiptCaptureWidget (stateless, driven by BLoC)
CaptureActionBottomSheet (camera / gallery options)
UploadProgressOverlay
MandatoryAttachmentBanner (liveRegion Semantics)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use image_picker 1.x API (ImagePicker().pickImage). Compress via flutter_image_compress or the built-in imageQuality parameter. Run compression in compute() to avoid jank. Implement ReceiptStorageAdapter as an abstract class with a SupabaseReceiptStorageAdapter implementation — this makes unit testing easy with a mock adapter.

The threshold check should be a pure function: bool isReceiptRequired(double amount, ExpenseThresholdConfig config) => amount >= config.receiptRequiredAbove. Dispatch ReceiptAttached/ReceiptRemoved events to ExpenseFormBloc so the BLoC owns the 'receipt required' state rather than the widget. Use StreamController or a BLoC stream for upload progress updates. For iOS, ensure NSCameraUsageDescription and NSPhotoLibraryUsageDescription are set in Info.plist; for Android, READ_EXTERNAL_STORAGE / READ_MEDIA_IMAGES permissions are declared in AndroidManifest.xml.

Testing Requirements

Unit tests: mock image_picker and receipt storage adapter; test compression logic with images of known sizes; test threshold detection for amounts below, at, and above threshold. Widget tests: mock PickedFile response, assert progress indicator appears and disappears; assert mandatory-attachment banner appears when amount > threshold and disappears when amount drops below. Integration test: end-to-end upload to Supabase Storage staging bucket, assert receipt URL is returned and stored in BLoC state. Permission-denial path: mock permission denial, assert inline fallback message is displayed.

Target ≥ 85% line coverage on capture and upload logic.

Component
Receipt Capture Widget
ui medium
Epic Risks (3)
medium impact medium prob dependency

The image_picker Flutter plugin requires platform-specific permissions (NSPhotoLibraryUsageDescription, camera permission) and behaves differently across iOS and Android versions. Permission denial or plugin misconfiguration can silently prevent receipt attachment.

Mitigation & Contingency

Mitigation: Configure all required permission strings in Info.plist and AndroidManifest.xml during initial plugin setup. Use the permission_handler package to check and request permissions before launching the picker, with clear user-facing explanations. Test on both platforms across at least two OS versions.

Contingency: If image_picker proves unreliable on a specific platform version, fall back to file_picker as an alternative that uses the OS document picker interface, which requires fewer permissions on some Android versions.

high impact medium prob technical

The expense form BLoC manages interconnected state across expense type selection, field visibility, receipt requirement, threshold evaluation, and submission flow. Incorrect state transitions can cause UI inconsistencies such as required receipt indicator not updating after amount change, or form appearing valid when mutual exclusion is violated.

Mitigation & Contingency

Mitigation: Model BLoC states as sealed classes with exhaustive pattern matching. Write state transition unit tests covering every combination of: type selection change, amount field change above/below threshold, receipt attachment/removal, and offline mode toggle. Use bloc_test for comprehensive state sequence assertions.

Contingency: If BLoC complexity becomes unmanageable, split into two BLoCs — one for type selection/exclusion state and one for field values/submission — coordinating via a parent provider, accepting the small overhead of inter-BLoC communication.

high impact medium prob technical

The expense type selector must enforce mutual exclusion visually by disabling options and showing conflict tooltips, while remaining fully accessible to screen reader users who cannot perceive visual disable states. Incorrect semantics labelling will fail WCAG 2.2 AA requirements critical for Blindeforbundet and HLF users.

Mitigation & Contingency

Mitigation: Use Flutter Semantics widgets to explicitly set disabled state and provide conflict explanations as semanticLabel strings on disabled options. Run accessibility audits with TalkBack and VoiceOver during widget development, not post-completion. Reference the project's accessibility test harness for required test coverage.

Contingency: If custom widget accessibility is difficult to certify, implement the selector as a standard Flutter Radio/Checkbox group with built-in accessibility semantics and an explanatory Text widget below each conflicting option, sacrificing visual elegance for guaranteed WCAG compliance.