medium priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

Widget accepts a Uint8List (compressed image bytes) and an onRemove VoidCallback as required parameters
While the image is decoding, a shimmer placeholder of the same dimensions is displayed
If image decoding fails, a fallback icon (e.g., broken-image outlined icon) is shown with an accessible label
A delete button (X icon) is visually overlaid in the top-right corner of the thumbnail with sufficient tap target (minimum 44×44 dp per WCAG 2.2 AA)
Semantics node label is exactly 'Receipt attached, double-tap to remove' for VoiceOver (iOS) and TalkBack (Android)
The semantics node wraps the entire thumbnail including the delete affordance as a single interactive element
Thumbnail dimensions are fixed at 80×80 dp by default with a configurable size parameter
Widget is stateless and pure — it owns no BLoC or Riverpod state
Golden test captures the three states: loading shimmer, image loaded, and error fallback
Widget passes flutter_test accessibility checks (no contrast failures, tap targets sufficient)

Technical Requirements

frameworks
Flutter
shimmer (Flutter package for loading placeholder)
performance requirements
Image must be displayed within one frame after bytes are available — use Image.memory with cacheWidth set to thumbnail pixel size to avoid large texture uploads
Widget rebuild must not re-decode the image on every parent rebuild — use const where possible and stable key
security requirements
Image bytes must not be logged or persisted from within this widget
onRemove callback must not expose the raw bytes to external systems
ui components
Shimmer placeholder (matching thumbnail dimensions)
Image.memory for decoded display
Stack with Positioned delete IconButton
Semantics wrapper node
Error fallback Icon widget

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use a Stack widget: bottom layer is Image.memory inside a ClipRRect for rounded corners (use design token border radius), top layer is a Positioned IconButton for delete. Wrap the whole Stack in a Semantics widget with label, onTap mapped to onRemove, and excludeSemantics: true on child widgets to prevent duplicate announcements. For the shimmer, use the shimmer package (already common in Flutter projects) with a Container of the same size as a child — the shimmer disappears the moment FutureBuilder or direct decoding completes. Match corner radius and shadow to the app's design token system (use AppTokens.radiusSm or equivalent).

The widget should be in the shared widget library alongside AppButton and AppTextField per the architecture description.

Testing Requirements

Unit/widget tests (flutter_test): pump widget with valid bytes and verify Image.memory is present; pump with null/empty bytes and verify fallback icon; verify Semantics node label text; tap the delete button and verify onRemove is called once. Golden tests: generate three golden images for loading, success, and error states. Accessibility test: use SemanticsHandle to assert the semantics tree contains the required label. Test on both iOS and Android screen sizes (375 and 412 dp width).

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.