Skip to content

Commit 29c2b90

Browse files
committed
Fix iOS share doing nothing: share directly from the tap
On iPhone, tapping a single-version Download chained two fetches off one tap (versions list, then the file). iOS only permits navigator.share() while the tap's transient activation is alive, so the second fetch killed it and the share rejected with AbortError — which we swallowed as a user cancel, hence nothing happened. Mobile now shares the default version straight from the tap via a single fetch in saveFile(), preserving activation; the version menu stays on desktop. When Web Share can't take the file, navigate to it so the browser's save UI shows instead of silently doing nothing.
1 parent f26e8c9 commit 29c2b90

2 files changed

Lines changed: 24 additions & 16 deletions

File tree

src/lib/components/download/DownloadVersionPicker.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { addToast } from '$lib/stores/toast.svelte';
33
import { formatBytes } from '$lib/utils/format';
4-
import { saveFile } from '$lib/utils/download';
4+
import { saveFile, isMobileDevice } from '$lib/utils/download';
55
import DownloadIcon from '$lib/components/icons/DownloadIcon.svelte';
66
77
type Version = {
@@ -76,6 +76,15 @@
7676
}
7777
7878
function handleClick() {
79+
// On mobile, share the file directly from this tap. A single fetch (inside
80+
// saveFile) keeps iOS transient activation alive so navigator.share() is
81+
// allowed; opening the version menu first would need a second fetch and the
82+
// share would silently fail. Mobile users get the default version — version
83+
// selection stays on desktop.
84+
if (isMobileDevice()) {
85+
void saveFile(downloadId);
86+
return;
87+
}
7988
if (open) {
8089
open = false;
8190
return;

src/lib/utils/download.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,25 @@ function filenameFromContentDisposition(header: string | null): string | null {
2424
return plain?.[1]?.trim() ?? null;
2525
}
2626

27-
function downloadViaAnchor(blob: Blob, name: string) {
28-
const objUrl = URL.createObjectURL(blob);
29-
const a = document.createElement('a');
30-
a.href = objUrl;
31-
a.download = name;
32-
document.body.appendChild(a);
33-
a.click();
34-
a.remove();
35-
setTimeout(() => URL.revokeObjectURL(objUrl), 10_000);
36-
}
37-
3827
/**
3928
* Save a downloaded file to the user's device.
4029
*
4130
* Desktop: opens the file in a new tab (the browser handles the download).
4231
* Mobile: fetches the real file and opens the native share sheet so users can
4332
* "Save to Photos" or send it to a contact. `canShare` is tested against the
4433
* *real* file (which carries a proper MIME type from the endpoint) — an empty,
45-
* type-less probe returns false on iOS WebKit. Falls back to a plain download
46-
* when the Web Share API is unavailable (e.g. Firefox on iOS).
34+
* type-less probe returns false on iOS WebKit.
35+
*
36+
* IMPORTANT (iOS): `navigator.share()` is only allowed while the tap's transient
37+
* user activation is alive. Callers MUST invoke this directly from the tap with
38+
* no awaits in between — the single `await fetch()` here is the only delay the
39+
* activation can survive. Chaining another fetch before this (e.g. loading a
40+
* version list first) loses activation and iOS rejects the share, usually with
41+
* AbortError, which is indistinguishable from the user dismissing the sheet.
42+
*
43+
* When the Web Share API can't take the file (older iOS, Firefox, unsupported
44+
* type), we navigate straight to the file so the browser's own save UI appears —
45+
* we never silently do nothing.
4746
*/
4847
export async function saveFile(id: string, opts?: { filename?: string }): Promise<void> {
4948
if (!isMobileDevice()) {
@@ -66,8 +65,8 @@ export async function saveFile(id: string, opts?: { filename?: string }): Promis
6665
return;
6766
}
6867

69-
// Share API (with files) unavailable → fall back to a real download.
70-
downloadViaAnchor(blob, name);
68+
// Web Share (with files) unsupported here → let the browser handle it.
69+
window.location.href = fileUrl(id);
7170
} catch (e: unknown) {
7271
// User dismissed the share sheet — not an error.
7372
if (e instanceof DOMException && e.name === 'AbortError') return;

0 commit comments

Comments
 (0)