high priority medium complexity frontend pending frontend specialist Tier 6

Acceptance Criteria

Confirm button tap calls _formKey.currentState!.validate() first; if false, submission is aborted and no service call is made
If validation passes, MileageClaimService.submitClaim(date, distanceKm, origin, destination?) is called as an async operation
A CircularProgressIndicator replaces or overlays the confirm button during the async call — button is not tappable during this state
The confirm button's onPressed is set to null during in-flight requests preventing double-submission
_isSubmitting bool state variable gates both the button enabled state and the loading indicator visibility
On successful submitClaim response, Navigator.pop(context, MileageSubmissionResult.success) is called to return a success result to the calling screen
On API error (network failure, server 4xx/5xx, Supabase error), an inline error banner appears at the top of the form (above the date field) with a plain-language error message and a dismiss button
The inline error banner uses design token error/warning colour styling and is NOT a SnackBar or AlertDialog
On server-side validation error (e.g., distance exceeds policy limit), the error message from the service response is surfaced in the inline banner
After error display, the form remains populated with user data — the user can correct and resubmit without re-entering all fields
_isSubmitting is reset to false in a finally block to guarantee button re-enablement even after errors
flutter_test: mock MileageClaimService, verify loading state appears during async call and success triggers pop

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
apis
MileageClaimService.submitClaim(DateTime date, double distanceKm, String origin, String? destination) -> Future<MileageClaimResult>
Supabase REST API (via MileageClaimService abstraction)
data models
MileageReimbursementEntry
MileageClaimResult
performance requirements
UI remains responsive during submission — submission runs on async/await without blocking the main isolate
Loading indicator appears within one frame of button tap
security requirements
All submitted data is validated client-side before reaching MileageClaimService
The Supabase call must use the authenticated user's session token — MileageClaimService must not accept unauthenticated calls
No sensitive user data (e.g., route details) is logged to the console in production builds
Network errors must not expose raw Supabase error messages to the user — wrap in user-friendly strings
ui components
CircularProgressIndicator (sized to match button height or overlaid on button)
AnimatedSwitcher or AnimatedVisibility for smooth loading/button transition
Inline error banner widget (custom Container or existing app ErrorBanner component) with dismiss capability
AppButton with disabled state (onPressed: null)

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Use a try/catch/finally pattern: `setState(() => _isSubmitting = true)` before the await, `finally { if (mounted) setState(() => _isSubmitting = false); }`. The `mounted` check is critical — the user may navigate away during submission. For the error banner: create a stateful `_errorMessage` String? in the form; when non-null, render a styled Container at the top of the scroll view with the message and an IconButton to dismiss (sets _errorMessage to null).

Do not use SnackBar — it is transient and inaccessible to screen readers. The banner must remain visible until explicitly dismissed. For Riverpod integration: if MileageClaimService is provided via a Riverpod provider, use ref.read(mileageClaimServiceProvider) inside the submit handler (read, not watch — submission is an action, not reactive state). For the success result, define a sealed class or enum `MileageSubmissionResult` so the calling screen can differentiate success from cancellation (Navigator.pop with no result).

Ensure HLF's automatic approval logic (under 50km or no expenses = auto-approve) is handled server-side in MileageClaimService — do not implement approval logic in the UI layer.

Testing Requirements

Write flutter_test widget tests with a mocked MileageClaimService: (1) Pump form with valid data, tap confirm — verify CircularProgressIndicator appears and button onPressed is null while future is pending. (2) Mock submitClaim to return success — verify Navigator.pop is called with success result using a NavigatorObserver mock. (3) Mock submitClaim to throw a network exception — verify inline error banner appears with appropriate message and no dialog/snackbar. (4) After error, verify form fields still contain the previously entered values.

(5) Verify _isSubmitting resets to false after both success and error paths (check button re-enabled after error). Use Completer in mock to control async timing. Integration test with TestFlight: manual verification that a real Supabase submission succeeds end-to-end in staging environment.

Component
Mileage Entry Form
ui medium
Epic Risks (3)
medium impact low prob technical

Triggering MileageCalculationService on every keystroke in the distance field could cause frame drops on lower-end Android devices if the widget rebuild chain is too broad. Jank during real-time calculation would degrade the peer-mentor experience, particularly for users with motor impairments who may type slowly and need immediate feedback.

Mitigation & Contingency

Mitigation: Keep the reimbursement calculation inside the RealtimeReimbursementDisplay widget subtree only, using a StatefulWidget with a local state variable rather than lifting state to the BLoC. This limits rebuilds to a single widget. Profile on a low-end device (Qualcomm Snapdragon 450 class) before code review.

Contingency: If profiling shows frame drops, debounce the onChanged callback by 50ms using a Timer before updating the local state, which remains imperceptible to the user while eliminating excessive rebuilds.

medium impact medium prob scope

The optional destination field requires balancing two competing requirements: WCAG 2.2 AA demands a clear label association for screen readers, while the privacy requirement means the label must communicate optionality and privacy sensitivity without exposing the user to undue pressure to fill it in. A poorly worded label could cause screen-reader users to misunderstand the field's optional nature or feel compelled to enter private location data.

Mitigation & Contingency

Mitigation: Draft label text and hint text with input from at least one screen-reader user during the accessibility review. Use Flutter's Semantics widget to provide a separate semantic label that is more descriptive than the visual placeholder. Test with TalkBack and VoiceOver before sign-off.

Contingency: If user testing reveals confusion, replace the inline hint with a tappable information icon that opens a brief explanation of why the field is optional and how the data is used, reducing cognitive load on the field label itself.

low impact medium prob integration

If OrgRateConfigRepository has not yet resolved the org rate when the form loads (e.g. slow network, first launch with empty cache), the reimbursement display cannot show a meaningful value. Showing '0' or an error state could confuse mentors and undermine trust in the feature.

Mitigation & Contingency

Mitigation: Display a loading skeleton in the reimbursement display widget until the rate is available. Once loaded, animate the value in. Show a subtle 'rate unavailable' message if the fetch times out, but allow form completion and submission (rate will be fetched server-side at submission time).

Contingency: If rate unavailability is frequent due to connectivity issues, store the last successfully fetched rate in SharedPreferences as an additional fallback so the widget can render a stale-but-indicative value with a caveat label.