mirror of
https://github.com/coder/coder.git
synced 2026-10-25 17:53:46 +08:00
On iOS, tapping an attachment download link inside the installed (standalone) PWA hands the file to QuickLook, which renders on top of the app with no reliable way back. Users had to force-quit the PWA to recover. ## Approach In iOS standalone mode, when the device supports file sharing, the download click is intercepted and the fetched attachment is handed to the native share sheet (`navigator.share` with a `File`), where the user can pick Save to Files or any other target. This is the platform-intended path for saving files from a PWA; no library covers it (FileSaver.js is broken in iOS PWAs, and browser-fs-access falls back to the same broken anchor path). Everything else keeps native behavior: - Non-iOS platforms and regular iOS Safari tabs keep the plain `<a download>` anchor. - iOS standalone devices without file sharing keep the anchor too. - Inline `data:` attachments are decoded locally because the production CSP (`connect-src 'self'`) blocks fetching them. ## Error model Deliberately small: a share-sheet dismissal (`AbortError`) is silent, an expired click gesture (`NotAllowedError`, which happens when the fetch outlasts transient activation) shows a toast whose Save action retries with a fresh gesture, and any other failure shows a plain error toast. An earlier revision carried pending-state tracking, abort-signal threading through unmount, and a blob-URL tab fallback; those guards were removed on purpose to keep the change proportionate to the defect. Also hardens chat file uploads slightly: filenames are sanitized before upload and the file-picker `accept` attribute mirrors the server's attachment allowlist. ## Testing - Storybook interaction tests cover the share happy path and native-anchor behavior; unit tests cover the intercept gate (including iPadOS detection), failure toasts, retry action, and inline-data decode. - Validated on an iPhone against a dev deployment: share sheet opens, Save to Files works, and the app remains usable afterwards. > Mux implemented this on Mike's behalf. <!-- mux-attribution: model=claude-sonnet-4-5 thinking=high -->