feat(dashboard): offline web push — service worker, subscribe flow, deep-link
Notifications phase 2: a /push-sw.js service worker shows a system notification on push and, on click, focuses an open CRM tab (or opens one) and deep-links to the thread. usePushNotifications registers the SW, requests permission, subscribes with IIOS's VAPID key, and stores the subscription via the be-crm door. The topbar bell becomes a real enable/disable control (hidden when push is unsupported/demo). Dashboard listens for the SW's notif-click message + a ?thread= deep link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/* Web Push service worker for CRM offline notifications.
|
||||
*
|
||||
* Receives push payloads from IIOS (WebPushDelivery), shows a system notification, and on click
|
||||
* focuses an open CRM tab (or opens one) and posts the thread to deep-link to. The payload shape is
|
||||
* IIOS's NotificationPayload: { title, body, data: { threadId, interactionId? } }.
|
||||
*
|
||||
* Served from /public at /push-sw.js → root scope ('/'), so it controls the whole app.
|
||||
*/
|
||||
|
||||
self.addEventListener("push", (event) => {
|
||||
let payload = {};
|
||||
try {
|
||||
payload = event.data ? event.data.json() : {};
|
||||
} catch {
|
||||
payload = { title: "New notification", body: event.data ? event.data.text() : "" };
|
||||
}
|
||||
const title = payload.title || "New message";
|
||||
const threadId = payload.data && payload.data.threadId;
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, {
|
||||
body: payload.body || "",
|
||||
// Coalesce repeated pings for the same thread into one notification.
|
||||
tag: threadId ? `thread:${threadId}` : undefined,
|
||||
renotify: Boolean(threadId),
|
||||
data: payload.data || {},
|
||||
icon: "/icons/i_bell.svg",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
event.notification.close();
|
||||
const threadId = event.notification.data && event.notification.data.threadId;
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
|
||||
// Prefer an already-open CRM tab: focus it and tell the app which thread to open.
|
||||
for (const client of all) {
|
||||
if ("focus" in client) {
|
||||
await client.focus();
|
||||
client.postMessage({ type: "notif-click", threadId: threadId || null });
|
||||
return;
|
||||
}
|
||||
}
|
||||
// No tab open — launch one deep-linked via the query string.
|
||||
const url = threadId ? `/dashboard?thread=${encodeURIComponent(threadId)}` : "/dashboard";
|
||||
if (self.clients.openWindow) await self.clients.openWindow(url);
|
||||
})(),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user