fix(tours): open the GPX share sheet inside the tap

Sharing a tour fetched the GPX file first and only then called
navigator.share, so the tap that asked had often run out by then. The
retry path kept that file for one more tap only, and a tap while a sheet
was still open made the browser throw InvalidStateError, which read as a
failed share.

The share button now fetches the file ahead where sharing is offered
(not while the planner has unsaved changes), and every tap hands a fresh
File built from the kept bytes to the sheet before anything is awaited.
A file a tap had to fetch is kept for the next tap. A tap while a sheet
is open is let be, and InvalidStateError and a closed sheet stay quiet;
only a real failure still says the download is left.
This commit is contained in:
Maurice
2026-10-20 20:43:28 +02:00
parent 7f14dde362
commit 94674e7572
5 changed files with 158 additions and 19 deletions
+3 -1
View File
@@ -41,13 +41,15 @@ describe('shareFile (IO-01)', () => {
expect(share).toHaveBeenCalledWith({ files: [file], title: 'Kramer' });
});
it('tells a closed sheet, a refused tap and any other failure apart', async () => {
it('tells a closed sheet, a refused tap, an open sheet and any other failure apart', async () => {
const share = vi.fn();
stubShare(share, () => true);
share.mockRejectedValueOnce(new DOMException('closed', 'AbortError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('cancelled');
share.mockRejectedValueOnce(new DOMException('no activation', 'NotAllowedError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('needs-gesture');
share.mockRejectedValueOnce(new DOMException('earlier share still open', 'InvalidStateError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('busy');
share.mockRejectedValueOnce(new DOMException('bad data', 'DataError'));
await expect(shareFile(gpx(), 'Kramer')).resolves.toBe('failed');
share.mockRejectedValueOnce(new Error('odd'));
+5 -2
View File
@@ -20,9 +20,11 @@ export function canShareFile(name: string, type: string): boolean {
/**
* How a share ended: `cancelled` when the sheet was closed without a target,
* `needs-gesture` when the browser refused because the tap that asked lies too
* far back (Safari, after the file had to be fetched first), `failed` otherwise.
* far back (Safari, after the file had to be fetched first), `busy` when an
* earlier sheet is still open (the browser's InvalidStateError), `failed`
* otherwise.
*/
export type FileShareOutcome = 'shared' | 'cancelled' | 'needs-gesture' | 'failed';
export type FileShareOutcome = 'shared' | 'cancelled' | 'needs-gesture' | 'busy' | 'failed';
/** Opens the share sheet with the file; call it straight from the tap that asks for it. */
export async function shareFile(file: File, title: string): Promise<FileShareOutcome> {
@@ -32,6 +34,7 @@ export async function shareFile(file: File, title: string): Promise<FileShareOut
} catch (error: unknown) {
if (error instanceof DOMException && error.name === 'AbortError') return 'cancelled';
if (error instanceof DOMException && error.name === 'NotAllowedError') return 'needs-gesture';
if (error instanceof DOMException && error.name === 'InvalidStateError') return 'busy';
return 'failed';
}
}