high priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

Widget accepts two nullable callback parameters: VoidCallback? onRemove and VoidCallback? onReplace
When onRemove is non-null, a Remove icon-button (Icons.delete_outline or similar) is rendered visibly on or adjacent to the thumbnail card
When onReplace is non-null, a Replace icon-button (Icons.camera_alt or Icons.swap_horiz) is rendered visibly on or adjacent to the thumbnail card
Tapping Remove triggers HapticFeedback.lightImpact() then calls onRemove callback — the widget itself does not manage state; state revert is the caller's responsibility
Tapping Replace triggers HapticFeedback.lightImpact() then calls onReplace callback
When both callbacks are null (e.g., during active upload), neither button is rendered
Both buttons have a minimum tap target of 44×44 dp enforced via SizedBox or IconButton constraints
Focus traversal order within the widget is: thumbnail image → Remove button → Replace button (left-to-right, top-to-bottom reading order)
Both buttons are reachable and activatable via keyboard (Enter/Space) and switch access without requiring custom FocusNode configuration by callers
Widget test verifies onRemove is called exactly once on tap, onReplace is called exactly once on tap, and neither button appears when callbacks are null

Technical Requirements

frameworks
Flutter
Dart
apis
HapticFeedback (services library)
data models
Receipt
performance requirements
Button rendering must be conditional (null check) so no invisible tappable areas exist when callbacks are null
security requirements
Remove action must not delete from Supabase storage within this widget — it only calls the callback; storage deletion is the responsibility of the BLoC/Riverpod layer
ui components
IconButton with explicit iconSize and constraints
HapticFeedback.lightImpact()
FocusTraversalOrder or ExcludeFocus as needed
Tooltip on each button for accessibility label

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Add onRemove and onReplace as optional named parameters to ReceiptThumbnailPreview. Position buttons as an overlay row at the bottom-right or top-right of the thumbnail container using Positioned within the existing Stack from task-002. Use IconButton with BoxConstraints(minWidth: 44, minHeight: 44) to guarantee tap targets. Wrap each button in a Tooltip with a descriptive message ('Remove receipt', 'Replace receipt') — this also satisfies the accessibility label requirement that task-004 will formalize with Semantics.

Do NOT implement any confirmation dialog in this task — haptic feedback is the only confirmation signal required per spec. The Receipt Camera Sheet opening is the caller's responsibility via the onReplace callback; this widget has no knowledge of navigation.

Testing Requirements

Write flutter_test widget tests: (1) both buttons visible when callbacks provided, (2) neither button visible when both callbacks null, (3) onRemove fires exactly once on Remove tap, (4) onReplace fires exactly once on Replace tap, (5) haptic feedback is triggered (mock HapticFeedback channel in test). For focus order, use FocusTraversalPolicy tests or manual verification notes in the PR since automated focus order testing in flutter_test is limited. Document manual switch-access test steps in PR description.

Epic Risks (2)
medium impact medium prob technical

Flutter's accessibility live region support (SemanticsProperties.liveRegion) has known inconsistencies between iOS VoiceOver and Android TalkBack, and between Flutter versions. Threshold-crossing announcements may fail to fire or double-fire, breaking the accessibility contract for Blindeforbundet users.

Mitigation & Contingency

Mitigation: Test live region announcements on physical devices with VoiceOver and TalkBack enabled from the first iteration. Use the AccessibilityLiveRegionAnnouncer component pattern already established in the project. Verify announcement timing relative to Bloc state emissions to avoid double-fires.

Contingency: If Flutter live regions prove unreliable, implement a platform-channel fallback that calls UIAccessibility.post(notification:) on iOS and AccessibilityManager.sendAccessibilityEvent() on Android directly, bypassing Flutter's abstraction.

medium impact low prob integration

The org-configurable threshold must be available at form-render time. If the threshold configuration is fetched asynchronously and not cached, the indicator may briefly show the wrong state (e.g., 'optional' before the threshold loads), confusing users and potentially allowing invalid submissions.

Mitigation & Contingency

Mitigation: Ensure the receipt threshold validator loads and caches the org configuration at app startup or organization selection time, not lazily on form open. Use a loading state in the indicator widget rather than defaulting to 'optional' while configuration is pending.

Contingency: If startup caching is not feasible, treat an unknown threshold as 'receipt required' (fail safe) and surface a clear loading indicator until the configuration resolves, preventing invalid submissions while the config loads.