high priority low complexity infrastructure pending infrastructure specialist Tier 2

Acceptance Criteria

downloadAndShare(signedUrl, fileName, mimeType) downloads the file bytes and invokes the native share/save sheet on both iOS and Android
A DownloadProgress stream emits progress events (0.0–1.0) that the calling UI can subscribe to
On iOS the system share sheet appears allowing AirDrop, Mail, Files app, and third-party apps
On Android the file is saved to the Downloads directory and a system notification confirms the save; the share sheet is also offered
Network error during download emits DownloadError.network state and does not leave a partial file on disk
Signed URL expiry (HTTP 403) is caught and emits DownloadError.urlExpired, prompting the caller to re-request a fresh signed URL
fileName sanitisation: replaces characters illegal on iOS/Android filesystems (/, \, :, *, ?, ", <, >, |) with underscores
Temporary file is written to the Flutter temp directory during download and moved/deleted after share sheet dismissal
Handler is tested on both iOS 16+ and Android 12+ device targets (or simulators in CI)
Widget test confirms that progress callbacks update a BLoC/Riverpod state visible to the parent screen

Technical Requirements

frameworks
Flutter
share_plus (^7.x)
http (for streaming byte download)
path_provider (temp and downloads directory)
Riverpod (state for progress/error)
apis
Supabase Storage signed URL (HTTPS GET)
Native iOS UIActivityViewController (via share_plus)
Native Android DownloadManager / MediaStore (via share_plus or open_file)
data models
DownloadProgress (progress: double, bytesReceived: int, totalBytes: int)
DownloadError (enum: network, urlExpired, insufficientStorage, unknown)
DownloadResult (filePath: String, mimeType: String)
performance requirements
Progress stream emits at minimum every 10% of file size or every 500ms, whichever comes first
Download must not block the UI thread — use Dart isolate or async streaming via http.Client.send()
Temporary file cleaned up within 30 seconds of share sheet dismissal to avoid accumulating export files in temp storage
security requirements
signedUrl must be validated as an HTTPS URL before issuing the HTTP request — reject plain HTTP
Downloaded file bytes must not be logged or written to any analytics sink
Temporary file permissions must be set to owner-read-only (0600 on iOS/Android sandbox) before writing
Signed URL is a one-time credential — do not cache or retry with the same URL after a 403 response
ui components
DownloadProgressIndicator (LinearProgressIndicator bound to DownloadProgress stream)
DownloadErrorSnackbar (error message with retry action)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use `http.Client.send(Request)` with a `StreamedResponse` to download bytes in chunks and update the DownloadProgress stream incrementally — do not use `http.get()` which buffers the entire response. Write chunks to a File sink in the Flutter temp directory using `IOSink`. After download completes, call `Share.shareXFiles([XFile(tempPath, mimeType: mimeType)], subject: fileName)` from share_plus for the cross-platform share sheet. On Android, share_plus delegates to the system intent which will include the Downloads option.

For cleanup, register a callback on the Share result Future — if the user dismisses without saving, delete the temp file immediately. Add a fallback: if share_plus throws PlatformException (e.g., no apps installed), open the file directly with `OpenFile.open(tempPath)` from the open_file package. Keep the handler stateless — all state (progress, error) lives in the Riverpod DownloadNotifier that calls this service.

Testing Requirements

Unit tests (flutter_test) for FileDownloadHandler: (1) successful download emits progress from 0.0 to 1.0 then complete; (2) HTTP 403 on signed URL emits DownloadError.urlExpired; (3) network timeout emits DownloadError.network; (4) fileName sanitisation replaces illegal characters; (5) temp file is deleted after handler completes. Widget tests for the calling screen verifying that Riverpod/BLoC state updates trigger progress indicator rebuild. Manual device testing on iOS simulator (share sheet appearance) and Android emulator API 31+ (Downloads save + notification). No automated e2e test for the native share sheet itself — document as a manual test case.

Component
File Download Handler
infrastructure low
Epic Risks (3)
high impact medium prob technical

NHF's three-level hierarchy (national / region / chapter) with 1,400 chapters may have edge cases such as chapters belonging to multiple regions, orphaned nodes, or missing parent links in the database. Incorrect scope expansion would silently under- or over-report activities, which could invalidate a Bufdir submission.

Mitigation & Contingency

Mitigation: Obtain a full hierarchy fixture export from NHF before implementation begins. Write exhaustive unit tests covering boundary cases: single chapter, full national roll-up, chapters with no activities, and chapters assigned to multiple regions. Validate resolver output against a known-good manual count.

Contingency: If hierarchy data quality is too poor for automated resolution at launch, implement a manual scope override in the coordinator UI that allows the coordinator to explicitly select org units from a tree picker, bypassing the resolver.

medium impact high prob dependency

The activity_type_configuration table may not cover all activity types currently in use, leaving a subset unmapped at launch. Bufdir submissions with unmapped categories will be incomplete and may be rejected by Bufdir.

Mitigation & Contingency

Mitigation: Run a query against production activity data before implementation to enumerate all distinct activity type IDs. Cross-reference with Bufdir's published category schema (request from Norse Digital Products). Flag every gap as a known issue and build the warning surface into the preview panel.

Contingency: Implement a fallback 'Other' category bucket for unmapped types and surface a prominent warning in the export preview requiring coordinator acknowledgement before proceeding. Log unmapped types for post-launch cleanup.

high impact low prob security

Supabase RLS policies on generated_reports and the storage bucket must enforce strict org isolation. A misconfigured policy could allow a coordinator from one organisation to read another organisation's export files, creating a serious data breach with GDPR implications.

Mitigation & Contingency

Mitigation: Write RLS integration tests that attempt cross-org reads with explicitly different JWT tokens and assert that all attempts return empty sets or 403 errors. Include RLS policy review in the pull request checklist. Use Supabase's built-in policy tester during development.

Contingency: If a policy gap is discovered post-deployment, immediately revoke all signed URLs for affected exports, audit the access log for unauthorised reads, and issue a coordinated disclosure to affected organisations per GDPR breach notification requirements.