high priority medium complexity frontend pending frontend specialist Tier 6

Acceptance Criteria

Each claim card exposes an Approve button and a Reject button (or equivalent icon buttons) at all times without requiring a swipe gesture
Tapping Approve or Reject opens a modal bottom sheet containing a plain-language confirmation message, one primary confirm button, and one cancel button
The bottom sheet uses SingleActionScreenLayout from the cognitive accessibility component library
Confirming optimistically moves the claim to the approved/rejected visual state immediately, before the network call completes
If the network call fails, the optimistic update is rolled back and a retryable error message is shown
On success, a Semantics live-region announcement is made: e.g. 'Claim approved for [submitter name]'
Swipe-to-reveal (Dismissible or custom gesture) exposes the same approve/reject actions as a secondary shortcut
Swipe actions are entirely optional; all functionality is reachable without gestures (WCAG 2.2 SC 2.5.1)
Touch targets for approve and reject buttons are at minimum 48×48 dp
Reject action is visually distinguishable from Approve (colour and/or icon) without relying on colour alone

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Dart
apis
ExpenseAttestationService.attest(claimId, decision: AttestationDecision.approved | rejected)
Supabase PostgREST (expense_claims update)
data models
ExpenseClaim
AttestationDecision (enum)
AttestationQueueState (optimistic update variant)
performance requirements
Bottom sheet must open within one frame (16 ms) of tap — no async work before showing the sheet
Optimistic update must reflect in the UI before the network round-trip completes
security requirements
Coordinator's user ID must be recorded as attested_by in the database update; the client must not pass this value — Supabase function or RLS trigger sets it server-side
Supabase RLS must prevent non-coordinator roles from calling the attest endpoint
ui components
AttestationBottomSheet
SwipeToRevealWrapper
ApproveButton
RejectButton
Semantics live-region wrapper

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Optimistic updates: when AttestClaimEvent is dispatched, immediately emit a new Loaded state with the claim's status updated locally, then fire the async service call. Wrap the service call in try/catch: on failure emit a rollback state restoring the previous status and include an error message. For the live-region announcement, use Flutter's SemanticsService.announce() or wrap the success message in a Semantics widget with liveRegion: true. For the swipe gesture, use Flutter's Dismissible widget with confirmDismiss to show the same bottom sheet rather than acting immediately — this avoids accidental swipes.

Ensure the bottom sheet is dismissible via the back button and back-swipe on Android (Navigator.pop) to comply with the 'back button rather than side-swipe' navigation principle from the accessibility requirements.

Testing Requirements

Widget tests: tap Approve button → bottom sheet appears with correct claim details; tap Cancel → sheet dismisses with no state change; tap Confirm → optimistic update applied and attest() called once. BLoC unit tests: AttestClaimEvent with approve decision → optimistic Loaded state update emitted before service call resolves; service success → no rollback; service failure → rollback state emitted with error. Accessibility tests: verify Semantics labels on approve/reject buttons include action and claim context; verify live-region Announcement widget fires on success; verify swipe gesture is not the sole path to approval (buttons exist). Golden test for bottom sheet layout at 1.0x and 2.0x font scale.

Component
Attestation Queue Screen
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.