high priority low complexity frontend pending frontend specialist Tier 4

Acceptance Criteria

When BLoC emits ExportSuccess state containing a signed URL, a SnackBar appears with a persistent 'Download' action button within 300ms
Tapping the SnackBar 'Download' button invokes FileDownloadHandler, which calls url_launcher to open the signed URL; on platforms without direct open support, the native share sheet is triggered instead
Tapping any row in ExportHistoryPanel fetches a fresh signed URL via the export run repository (not a cached/expired URL) before invoking FileDownloadHandler
The SnackBar Download action widget has a Semantics label 'Download export file' readable by VoiceOver and TalkBack
If url_launcher canLaunchUrl returns false, a fallback error SnackBar is shown with message 'Could not open file. Try again or contact support.'
Signed URL fetch failure on history row tap shows an inline error SnackBar with retry option; the tap target remains accessible
FileDownloadHandler is a standalone class injectable via constructor — not hardcoded inside the widget — to allow unit testing
SnackBar action button meets minimum 44×44 dp touch target requirement
Signed URLs are never stored in local state beyond the duration of the SnackBar display; no URL leaks to logs

Technical Requirements

frameworks
Flutter
BLoC
apis
url_launcher (canLaunchUrl, launchUrl, LaunchMode.externalApplication)
Supabase Storage signed URL API
Share Plus (platform share sheet fallback)
data models
ExportRun
ExportSuccess (BLoC state)
SignedFileUrl
performance requirements
Fresh signed URL fetched from repository within 2 seconds of row tap
SnackBar appears within one frame after ExportSuccess state emission
No blocking UI operations — URL fetch must be async with loading indicator on the tapped row
security requirements
Signed URLs must have short expiry (≤15 minutes); regenerate on each download attempt
Do not log or persist signed URLs in analytics events or crash reporters
Validate URL scheme before launching (must be https://)
ui components
ScaffoldMessenger.showSnackBar with SnackBarAction
FileDownloadHandler (new injectable class)
ExportHistoryPanel row loading state indicator
Semantics widget wrapping SnackBarAction

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Create FileDownloadHandler as a plain Dart class with a single async download(String url) method. Inject it into AccountingExportScreen via constructor parameter with a default instance so existing call sites don't break. Use BlocListener (not BlocBuilder) for the ExportSuccess side effect to avoid re-triggering on rebuilds. For the ExportHistoryPanel row tap, dispatch a FetchHistoryDownloadUrl event to the BLoC — do not call the repository directly from the widget.

Guard against double-taps by disabling the row or showing a CircularProgressIndicator in place of the download icon during the URL fetch. Use LaunchMode.externalApplication so files open in the user's preferred viewer rather than an in-app WebView. On iOS, if the file is a CSV/XLSX, url_launcher will hand off to Files.app or Numbers — test both file types on a real device.

Testing Requirements

Unit tests: FileDownloadHandler with mocked url_launcher — verify launchUrl called with correct URL and LaunchMode; verify share sheet invoked when canLaunchUrl is false; verify error SnackBar shown on exception. Widget tests: AccountingExportScreen pumped with ExportSuccess state — assert SnackBar visible with correct label; assert Semantics label present. ExportHistoryPanel widget test — tap row, verify repository fetchSignedUrl called, verify FileDownloadHandler.download called with returned URL. All tests use mocktail for url_launcher and repository stubs.

Target: 100% branch coverage on FileDownloadHandler.

Component
Export History Panel
ui low
Epic Risks (2)
medium impact medium prob technical

Export operations may take several seconds, and the UI must handle all intermediate states (loading, partial success, failure, duplicate warning) without leaving the coordinator on a blank or unresponsive screen. Missing state handling causes confusion and potentially double-submissions.

Mitigation & Contingency

Mitigation: Design the BLoC state machine with explicit states for each transition before writing any widget code: ExportIdle, ExportDuplicateWarning, ExportInProgress, ExportSuccess, ExportPartialSuccess, ExportFailed. Each state maps to a distinct UI. Widget tests cover all states.

Contingency: If a loading state is missed in production, surface a generic error state with a retry action rather than leaving the UI stuck. Add a timeout on the Edge Function call (default 30 seconds) that transitions to ExportFailed with a user-readable message.

high impact medium prob technical

The custom Export Date Range Picker may not be fully navigable with VoiceOver if the underlying Flutter date widgets do not expose the correct semantic tree. This is a critical accessibility failure for Blindeforbundet users who rely on screen readers.

Mitigation & Contingency

Mitigation: Use Flutter's built-in DateRangePicker as the base and wrap with explicit Semantics nodes for start and end labels. Test with VoiceOver on a physical iOS device as part of the definition of done for this component. Reference the existing AccessibilityTestHarness pattern used elsewhere in the app.

Contingency: If the custom picker fails accessibility audit, replace it with two independent DatePicker fields (start and end) using Flutter's standard accessible date input, which has broader VoiceOver support than range variants.