From 7ee0cbfac9dc77631bda4701ad8e4465433b3879 Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Tue, 27 Oct 2026 11:06:14 -0400 Subject: [PATCH] feat(gui): the service worker tells open tabs when the push subscription changes --- src/gui/src/sw.js | 15 +++++++++++++++ src/gui/src/sw.test.js | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/gui/src/sw.js b/src/gui/src/sw.js index 5b8d14d1e..f04cb8b13 100644 --- a/src/gui/src/sw.js +++ b/src/gui/src/sw.js @@ -20,12 +20,15 @@ // Shows each push as a notification and routes taps to a Puter window. // Push payload: JSON whose `puter` (or `data.puter`, an object or JSON string) // is { uid, title, body, icon, group, target: { app, path }, data }. +// On `pushsubscriptionchange` it posts { type: 'puter-push-subscription-changed' } +// to this origin's windows, so a signed-in page can register again. const DEFAULT_TITLE = 'New notification'; const DEFAULT_ICON = '/favicons/android-icon-192x192.png'; const SHOW_MESSAGE = 'puter-show-notification'; const CLICK_MESSAGE = 'puter-notification-click'; const CLICK_ACK_MESSAGE = 'puter-notification-click-ack'; +const SUBSCRIPTION_CHANGED_MESSAGE = 'puter-push-subscription-changed'; const CLICK_PARAM = 'puter.notification'; // Browsers allow openWindow only shortly after the click. const CLICK_ACK_BUDGET_MS = 600; @@ -189,6 +192,14 @@ async function showPageMessage (scope, message) { await showMessage(scope.registration, message, scope.location.origin); } +async function announceSubscriptionChange (scope) { + const origin = scope.location.origin; + const windowClients = await scope.clients.matchAll({ type: 'window', includeUncontrolled: true }); + for ( const client of windowClients ) { + if ( sameOriginUrl(client.url, origin) ) client.postMessage({ type: SUBSCRIPTION_CHANGED_MESSAGE }); + } +} + self.addEventListener('install', () => { self.skipWaiting(); }); @@ -216,3 +227,7 @@ self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil(routeClick(self, clickFromData(event.notification.data), startedAt)); }); + +self.addEventListener('pushsubscriptionchange', (event) => { + event.waitUntil(announceSubscriptionChange(self)); +}); diff --git a/src/gui/src/sw.test.js b/src/gui/src/sw.test.js index 88781bd77..d98c2d83a 100644 --- a/src/gui/src/sw.test.js +++ b/src/gui/src/sw.test.js @@ -303,6 +303,22 @@ describe('service worker notificationclick', () => { }); }); +describe('service worker pushsubscriptionchange', () => { + it('tells every window on this origin, and no other', async () => { + const desktop = windowClient(`${ORIGIN}/desktop`); + const hidden = windowClient(`${ORIGIN}/`); + const other = windowClient('https://elsewhere.example/'); + const worker = loadWorker({ windows: [desktop, hidden, other] }); + await worker.fire('pushsubscriptionchange', {}); + expect(worker.scope.clients.matchAll).toHaveBeenCalledWith({ type: 'window', includeUncontrolled: true }); + for ( const win of [desktop, hidden] ) { + expect(win.postMessage).toHaveBeenCalledWith({ type: 'puter-push-subscription-changed' }); + } + expect(other.postMessage).not.toHaveBeenCalled(); + expect(worker.shown).toEqual([]); + }); +}); + describe('service worker lifecycle', () => { it('takes over at once and adds no fetch handler', async () => { const worker = loadWorker();