high priority medium complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

JPEG and PNG content is loaded from the signed URL using an async network image loader and displayed inside an InteractiveViewer
User can pinch-to-zoom the image; minimum scale 0.5x, maximum scale 4.0x
User can pan the image when zoomed in (InteractiveViewer handles this natively)
PDF content is rendered in a scrollable multi-page view using flutter_pdfview or syncfusion_flutter_pdfviewer
PDF viewer loads from the signed URL (remote URL or downloaded temp file as required by the chosen package)
All PDF pages are scrollable vertically within the modal
A page indicator (e.g. 'Page 2 / 10') is shown for PDFs when the package provides a page-change callback
When image loading fails (network error, invalid URL), an error state widget is shown with an icon and a 'Could not load image' message
When PDF loading fails, an equivalent error state widget is shown with a 'Could not load document' message
Unsupported MIME type (neither image nor PDF) renders the error state widget
Error state widget provides a retry button that re-triggers content loading
Loading transitions are smooth — no janky layout shifts when switching from placeholder to content

Technical Requirements

frameworks
Flutter
InteractiveViewer (Flutter SDK built-in)
flutter_pdfview or syncfusion_flutter_pdfviewer for PDF rendering
cached_network_image or Image.network for image loading
apis
Signed URL from AttachmentPreviewModal constructor parameter
data models
AttachmentPreviewArgs (signedUrl, mimeType)
performance requirements
Image must start loading immediately on mount — no user interaction required to begin fetch
PDF should stream/load progressively; show per-page loading indicator if the package supports it
Memory: large images must use ResizeImage or cacheWidth/cacheHeight to avoid OOM on low-end devices
security requirements
PDF temp file (if downloaded to disk) must be written to app's temp directory and deleted on modal close
Signed URL must not be stored in SharedPreferences or any persistent store
Validate mimeType before passing URL to renderers to prevent renderer abuse
ui components
InteractiveViewer wrapping Image or CachedNetworkImage
flutter_pdfview (PDFView widget) or SfPdfViewer for PDF
Column with Icon + Text + TextButton for error state
Text widget for PDF page indicator
AnimatedSwitcher for smooth loading→content transition

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement a _buildContent(String mimeType, String signedUrl) factory method inside AttachmentPreviewModal that returns the appropriate child widget based on mimeType. Use a switch on mimeType: 'image/jpeg' | 'image/png' → ImagePreviewWidget; 'application/pdf' → PdfPreviewWidget; default → UnsupportedTypeWidget. For ImagePreviewWidget, use CachedNetworkImage with a loadingBuilder (show placeholder) and errorBuilder (show error state). Wrap it in InteractiveViewer with minScale: 0.5, maxScale: 4.0, clipBehavior: Clip.none.

For PdfPreviewWidget, if using flutter_pdfview, pass the signed URL to PDFView(filePath: ...) after downloading to a temp file with http package or dio, then delete the file in dispose(). If using syncfusion_flutter_pdfviewer, SfPdfViewer.network(signedUrl) is simpler but requires a license for commercial use — confirm with the team. Wrap the entire content area in AnimatedSwitcher with a FadeTransition for smooth loading→content animation.

Testing Requirements

Widget tests: verify InteractiveViewer is in the tree when mimeType is image/jpeg or image/png; verify PDF viewer widget is in the tree when mimeType is application/pdf; verify error state widget appears when image loader throws (use a mocked image provider); verify error state widget appears for unsupported mimeType; verify retry button in error state triggers reload. Integration test (optional, TestFlight): manually verify pinch-to-zoom on a real device. Unit test: mimeType routing function — given each mimeType, returns correct ContentRenderStrategy enum value.

Component
Attachment Preview Modal
ui low
Epic Risks (4)
medium impact medium prob dependency

Flutter does not include a first-party PDF renderer. Third-party packages (e.g., flutter_pdfview, syncfusion_flutter_pdf) have inconsistent accessibility support, may not honour dynamic type scaling, and can introduce large binary size increases. Choosing the wrong package late in development risks rework or an inaccessible PDF experience for Blindeforbundet users.

Mitigation & Contingency

Mitigation: Evaluate and spike two PDF rendering packages (flutter_pdfview and pdfx) in the first task of this epic before committing to implementation. Criteria: VoiceOver compatibility, dynamic type support, APK/IPA size delta, and licence. Document the decision in an ADR.

Contingency: If no package meets accessibility requirements, fall back to opening PDFs in the system browser via url_launcher, which inherits the OS's accessible PDF viewer. Display a clear 'Opening in external viewer' message to set user expectation.

medium impact medium prob technical

Flutter's Semantics API for live region announcements (analogous to aria-live) has known gaps on Android for dynamic content updates. Upload progress announcements required by the accessibility user story may not fire reliably on Android devices used by HLF and NHF members.

Mitigation & Contingency

Mitigation: Use SemanticsService.announce() for imperative announcements at each upload state transition rather than relying on declarative Semantics widget tree updates. Test on a physical Android device with TalkBack enabled during development, not only on iOS with VoiceOver.

Contingency: If SemanticsService.announce() proves unreliable, implement a persistent accessible status banner at the top of the screen that reflects the current upload state as plain text, satisfying the WCAG success criterion through a visual + programmatic mechanism.

low impact high prob technical

iOS and Android file picker behaviour diverges significantly: MIME type filtering works reliably on Android but is advisory on iOS (users can still navigate to and select non-compliant files via the Files app). This could allow unsupported file types to reach the upload service, causing validation failures with a confusing user experience.

Mitigation & Contingency

Mitigation: Rely on the AttachmentUploadService (Epic 2) as the authoritative MIME validation gate, regardless of platform. In the UI, re-validate the picked file's extension/MIME after selection and show an inline plain-language error ('Only PDF, JPEG, and PNG files are supported') before even calling the service.

Contingency: If users consistently hit the error due to iOS file picker limitations, add a user-facing help tooltip on the picker sheet explaining the supported file types, reducing support volume while the underlying OS limitation persists.

high impact medium prob scope

WCAG 2.2 AA compliance for three new interactive components (picker, grid, modal) with file system integration, role-gated actions, and live regions is a significant accessibility surface area. An incomplete audit before release could result in the feature being unusable for Blindeforbundet screen reader users at launch.

Mitigation & Contingency

Mitigation: Allocate a dedicated accessibility audit task at the end of the epic using the accessibility-test-harness (619) and wcag-compliance-checker (620) components already in the project. Include at least one manual test session on a physical device with VoiceOver enabled. Fail the epic's definition of done if any WCAG 2.2 AA violation remains open.

Contingency: If critical accessibility issues are found late, gate the feature behind the org-level attachments_enabled feature flag and only enable it for NHF (the requesting org) after a targeted fix cycle, rather than delaying the release for all organisations.