From d27ee166bc810a62bae62bc70b3637e9f740838e Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 24 Mar 2026 05:30:48 -0600 Subject: [PATCH] Notification Fixes (#22599) * Fix iOS having notification token revoked * Try to handle iOS stacked notifications * Fix typo * Improve updating of notification script --- web/public/notifications-worker.js | 31 +++++++++++++++++-- .../NotificationsSettingsExtras.tsx | 8 +++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/web/public/notifications-worker.js b/web/public/notifications-worker.js index ba4e033ea..b42a32a56 100644 --- a/web/public/notifications-worker.js +++ b/web/public/notifications-worker.js @@ -19,8 +19,7 @@ self.addEventListener("push", function (event) { break; } - // @ts-expect-error we know this exists - self.registration.showNotification(data.title, { + const notificationOptions = { body: data.message, icon: "/images/maskable-icon.png", image: data.image, @@ -28,7 +27,33 @@ self.addEventListener("push", function (event) { tag: data.id, data: { id: data.id, link: data.direct_url }, actions, - }); + }; + + // iOS Safari does not auto-coalesce notifications by tag (WebKit bug #258922). + // On iOS 18.3+ close() works, so we manually close duplicates before showing. + // On other platforms, tag-based replacement works natively — skip the extra work. + const isIOS = + /iPad|iPhone|iPod/.test(navigator.userAgent) && !self.MSStream; + + const show = () => + // @ts-expect-error we know this exists + self.registration.showNotification(data.title, notificationOptions); + + // event.waitUntil is required on iOS Safari — without it, the browser + // may consider this a "silent push" and revoke the subscription after 3 occurrences. + event.waitUntil( + isIOS + ? // @ts-expect-error we know this exists + self.registration + .getNotifications({ tag: data.id }) + .then((existing) => { + for (const n of existing) { + n.close(); + } + }) + .then(show) + : show(), // eslint-disable-line comma-dangle + ); } else { // pass // This push event has no data diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index f9fb6addf..0f186e105 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -58,7 +58,7 @@ import type { ConfigSectionData, JsonObject } from "@/types/configForm"; import { sanitizeSectionData } from "@/utils/configUtil"; import type { SectionRendererProps } from "./registry"; -const NOTIFICATION_SERVICE_WORKER = "/notification-worker.js"; +const NOTIFICATION_SERVICE_WORKER = "/notifications-worker.js"; import { SettingsGroupCard, SPLIT_ROW_CLASS_NAME, @@ -126,6 +126,8 @@ export default function NotificationsSettingsExtras({ .getRegistration(NOTIFICATION_SERVICE_WORKER) .then((worker) => { if (worker) { + // Trigger a check for an updated service worker script + worker.update().catch(() => {}); setRegistration(worker); } else { setRegistration(null); @@ -633,7 +635,9 @@ export default function NotificationsSettingsExtras({ Notification.requestPermission().then((permission) => { if (permission === "granted") { navigator.serviceWorker - .register(NOTIFICATION_SERVICE_WORKER) + .register(NOTIFICATION_SERVICE_WORKER, { + updateViaCache: "none", + }) .then((workerRegistration) => { setRegistration(workerRegistration);