From 244e8b7b5aa10b049246ca51daed5d3eba02001d Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 2 Nov 2026 20:15:34 +0100 Subject: [PATCH] fix: keep the login and mailbox owner in notification links on safari 16 #1499 --- lib/__tests__/helpers/push-click.ts | 8 ++++++-- lib/__tests__/sw-push-owner.test.ts | 5 +++++ public/sw.js | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/__tests__/helpers/push-click.ts b/lib/__tests__/helpers/push-click.ts index 7509a9c73..369efd6a2 100644 --- a/lib/__tests__/helpers/push-click.ts +++ b/lib/__tests__/helpers/push-click.ts @@ -5,7 +5,7 @@ import vm from 'node:vm'; export async function pushClick( accountId: string, preview: (url: string) => Promise, - options: { warm?: boolean; basePath?: string; legacy?: boolean; emailId?: string } = {}, + options: { warm?: boolean; basePath?: string; legacy?: boolean; emailId?: string; noParamsSize?: boolean } = {}, ): Promise { const source = readFileSync(resolve('public/sw.js'), 'utf8'); let notification: { data: unknown } | undefined; @@ -18,8 +18,12 @@ export async function pushClick( async focus() { return this; }, async navigate(url: string) { target = url; return this; }, }; + // Safari before 17 has no URLSearchParams.size. + const SearchParams = options.noParamsSize + ? class extends URLSearchParams { get size(): number { return undefined as unknown as number; } } + : URLSearchParams; const context = vm.createContext({ - URL, URLSearchParams, Response, AbortController, setTimeout, clearTimeout, console, + URL, URLSearchParams: SearchParams, Response, AbortController, setTimeout, clearTimeout, console, self: { location: { href: `https://mail.example.test${options.basePath ?? ''}/sw.js`, origin: 'https://mail.example.test' }, addEventListener: (type: string, fn: (event: unknown) => void) => listeners.set(type, fn), diff --git a/lib/__tests__/sw-push-owner.test.ts b/lib/__tests__/sw-push-owner.test.ts index ace7cf58d..ade1e246a 100644 --- a/lib/__tests__/sw-push-owner.test.ts +++ b/lib/__tests__/sw-push-owner.test.ts @@ -47,6 +47,11 @@ describe('push click keeps the owning mailbox (#1491)', () => { expect(parseTarget(target)).toMatchObject({ kind: 'folder', ownerAccountId: 'team', slot: 2 }); }); + it('keeps the slot and owner where URLSearchParams has no size', async () => { + const target = await pushClick('team', async () => ({ slot: 1, unreadTotal: 1, email: { id: 'same-id' } }), { noParamsSize: true }); + expect(parseTarget(target)).toMatchObject({ kind: 'message', ownerAccountId: 'team', slot: 1 }); + }); + it('does not fall back to a personal folder for an unavailable owner', () => { expect(resolveFolderRef('inbox', folders, 'unavailable')).toBeNull(); }); diff --git a/public/sw.js b/public/sw.js index 975db0c27..3dc7ff7b8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -925,7 +925,9 @@ function buildClickUrl(data) { const params = new URLSearchParams(); if (Number.isInteger(data.slot)) params.set("slot", String(data.slot)); if (typeof data.accountId === "string" && data.accountId) params.set("owner", data.accountId); - const targetQuery = params.size ? `?${params}` : ""; + // Not params.size: Safari before 17 (iOS 16.4+ web push) lacks it. + const qs = params.toString(); + const targetQuery = qs ? `?${qs}` : ""; if (data.kind === "email" && data.emailId) { // Permalink (#733). Under NEXT_PUBLIC_LOCALE_PREFIX=always the proxy // redirects this to the localised path; the worker has no locale to add.