medium priority medium complexity integration pending integration specialist Tier 2

Acceptance Criteria

Tapping the Re-download button begins URL resolution and shows an inline CircularProgressIndicator replacing the icon
Repository first attempts to use the fileUrl stored in ExportHistoryEntry
If the stored URL returns HTTP 403 or 404 (expired signed URL), repository automatically calls Supabase Storage createSignedUrl with a 1-hour expiry and returns the fresh URL
On successful URL resolution, share_plus SharePlus.share is invoked with the resolved URL and a descriptive subject line (e.g. 'Bufdir Export — Q1 2026')
On iOS, this triggers UIActivityViewController; on Android, it triggers Intent.ACTION_SEND
If URL resolution fails after retry, an inline error snackbar is shown with 'Download failed. Try again.' and a Retry action
Progress indicator is removed and the Re-download icon is restored whether the operation succeeds or fails
Only one re-download operation can be in progress at a time per list item — the button is disabled while loading
The re-download flow does not navigate away from the history list
Signed URL generation uses the Supabase Storage bucket path derived from the export audit log record, never a user-provided path
Integration test verifies the full flow: button tap → URL resolution (mocked) → share sheet trigger

Technical Requirements

frameworks
Flutter
Riverpod
apis
Supabase Storage (createSignedUrl for re-generating expired URLs)
share_plus (platform share sheet trigger)
data models
bufdir_export_audit_log
performance requirements
Signed URL generation must complete within 3 seconds — show progress indicator beyond 500ms to prevent perceived freeze
Do not download the file to device storage before sharing — share the URL directly to avoid large temporary files
security requirements
Signed URLs must always be generated server-side or directly via the authenticated Supabase SDK with the user's JWT — never using the service role key
Signed URL expiry must be set to a maximum of 1 hour (3600 seconds)
The file bucket path used for signed URL generation must be validated against the authenticated user's organisation scope before calling createSignedUrl
Per-bucket RLS policies on Supabase Storage must prevent cross-organisation file access
Receipt/export files containing PII are in private buckets — public URL access is not permitted
ui components
Re-download icon button with loading state
Inline CircularProgressIndicator
ScaffoldMessenger SnackBar for errors
share_plus SharePlus.share

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement RedownloadService as a separate injectable class to keep the list item widget thin. In the service: (1) attempt HEAD request on stored fileUrl to check validity, or catch the Supabase StorageException on download attempt, (2) on 403/expired error, call supabaseClient.storage.from(bucketName).createSignedUrl(filePath, 3600). Parse the filePath from the stored fileUrl — Supabase Storage signed URLs contain the bucket path as a query parameter or path segment. Use share_plus's SharePlus.share(url, subject: subject) — do NOT use SharePlus.shareFiles as it requires downloading to disk first.

Manage the per-item loading state in a local ValueNotifier inside the list item widget to avoid rebuilding the entire list on each tap. Use a try/finally block to guarantee the loading indicator is always cleared.

Testing Requirements

Write unit and widget tests covering: (1) successful URL resolution invokes share_plus with correct URL and subject, (2) HTTP 403 on stored URL triggers createSignedUrl and retries with fresh URL, (3) createSignedUrl failure shows error snackbar with Retry, (4) button is disabled during active resolution, (5) progress indicator appears and disappears correctly. Mock Supabase Storage client and share_plus plugin. Write an integration test on a device/emulator verifying the share sheet appears (can be dismissed). Test both iOS and Android paths if CI supports both simulators.

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

For large exports that run for 10–30 seconds, a static loading spinner will feel broken to users on slow mobile connections. If the UI cannot display meaningful progress during the export pipeline, coordinators may abandon the flow or trigger duplicate exports by pressing the button multiple times.

Mitigation & Contingency

Mitigation: Implement streaming progress events from the orchestrator BLoC through named pipeline stages (querying, mapping, generating, uploading). Display each stage label with a progress indicator on the trigger screen. Disable the generate button immediately on first tap to prevent duplicates.

Contingency: If streaming pipeline progress is not feasible in the first release, implement a deterministic stage-based progress animation (10% querying, 50% generating, 90% uploading) that gives users feedback without requiring real server events.

high impact medium prob technical

Custom date range pickers are among the most common accessibility failures in mobile apps. Blindeforbundet users rely on VoiceOver, and NHF users include people with cognitive impairments. A non-accessible period picker could make the entire export workflow unusable for a significant portion of the intended user base.

Mitigation & Contingency

Mitigation: Build the period picker using Flutter's native date picker semantics as the foundation, with preset shortcuts as primary navigation (reducing the need to interact with the custom range picker at all). Test with VoiceOver on iOS and TalkBack on Android before UI epic sign-off. Engage Blindeforbundet's test contact for accessibility validation.

Contingency: If the custom date range picker cannot be made fully accessible before release, ship only the preset period shortcuts (covering the majority of use cases) and add the custom range picker in a follow-up sprint after dedicated accessibility remediation.