medium priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

After a receipt is captured/uploaded, a thumbnail (min 80×80 logical pixels) is rendered directly below the capture button.
Tapping the thumbnail opens the full-resolution image in a modal viewer (InteractiveViewer or equivalent).
A remove button (X icon) is visible on or adjacent to the thumbnail at all times when a receipt is attached.
Tapping remove shows a confirmation dialog ('Remove receipt? This cannot be undone.') before deleting.
On confirmed removal, the thumbnail disappears and a live-region Semantics announcement says 'Receipt removed'.
On cancelled removal, focus returns to the remove button with no state change.
A replace affordance (re-tap capture area or secondary 'Replace' button) is available so users do not need to remove before capturing a new image.
The thumbnail has a Semantics label of 'Receipt image, tap to view full size'.
The remove button has a Semantics label of 'Remove receipt'.
After removal, if the expense amount is above threshold, the mandatory-attachment banner re-appears and is announced via its live region.
All new interactive elements meet 44×44dp touch target minimum.
No memory leaks: dispose of image file handles and controllers when the widget is removed from the tree.

Technical Requirements

frameworks
Flutter
data models
ReceiptAttachment
ExpenseFormState
performance requirements
Thumbnail is rendered from the already-compressed local file; no additional network request on display.
Modal viewer opens within one animation frame (<16 ms) from tap.
security requirements
Thumbnail renders from the local compressed copy only — never fetches the remote URL for the preview to avoid unnecessary data exposure.
Confirmation dialog prevents accidental deletion.
ui components
ReceiptThumbnailStrip
ReceiptFullScreenModal (InteractiveViewer)
RemoveConfirmationDialog
Semantics (liveRegion: true) for removal announcement

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Keep thumbnail state in ExpenseFormBloc (store ReceiptAttachment with localPath and uploadedUrl). The thumbnail widget reads localPath from state and uses Image.file() for display — no extra network call. Implement ReceiptFullScreenModal as a Hero-animated route wrapping InteractiveViewer(child: Image.file(...)). For the confirmation dialog, use showDialog() with a standard AlertDialog; dispatch ReceiptRemoved to the BLoC only if the user confirms.

For the live region on removal, use a dedicated Semantics(liveRegion: true) widget whose label key switches between '' and 'Receipt removed' — toggling the key forces re-announcement. Clean up localPath File reference in the BLoC's close() method.

Testing Requirements

Widget tests: verify thumbnail visible after ReceiptAttached state; tap remove → assert dialog shown; confirm → assert thumbnail hidden and live-region label updated; cancel → assert thumbnail still visible. Test full-screen modal opens on thumbnail tap. Test replace flow: tap capture area with existing receipt → new receipt replaces old without confirmation. Accessibility: SemanticsController.find() for thumbnail label and remove button label.

Memory test: pump and dispose widget, assert no pending image file handles remain.

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.