// Web Push enablement for the current device. The service worker itself is // registered globally by nav.js; this module owns the *subscription* handshake: // ask permission, subscribe with the server's VAPID key, and register the // resulting endpoint against the signed-in user (via account.js's cookie-aware // apiFetch). Used by the /alerts page to render a "Notifications on this device" // control. // // Push + service workers require a secure context (HTTPS, or http://localhost), // so on a plain-HTTP LAN origin this cleanly reports "unsupported" instead of // throwing. import { apiFetch } from "./account.js"; export function supported() { return ( typeof window !== "undefined" && window.isSecureContext && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window ); } // The applicationServerKey must be a Uint8Array of the base64url-decoded VAPID // public key. function urlB64ToUint8Array(base64) { const padding = "=".repeat((4 - (base64.length % 4)) % 4); const b64 = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/"); const raw = atob(b64); const out = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); return out; } async function registration() { // nav.js registered sw.js already; ready resolves once it's active. return navigator.serviceWorker.ready; } // Is this device currently subscribed? (a PushSubscription exists locally) export async function isEnabled() { if (!supported()) return false; try { const reg = await registration(); return !!(await reg.pushManager.getSubscription()); } catch (e) { return false; } } export function permission() { return supported() ? Notification.permission : "unsupported"; } // Subscribe this device and register it server-side. Throws on failure so the // caller can surface a message. export async function enable() { if (!supported()) throw new Error("This browser doesn't support push notifications."); const perm = await Notification.requestPermission(); if (perm !== "granted") throw new Error("Notification permission was not granted."); const reg = await registration(); let sub = await reg.pushManager.getSubscription(); if (!sub) { const res = await apiFetch("api/v2/push/vapid-key"); if (!res.ok) throw new Error("Couldn't fetch the server key."); const { key } = await res.json(); sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64ToUint8Array(key), }); } const res = await apiFetch("api/v2/push/subscribe", { method: "POST", json: sub.toJSON() }); if (!res.ok) throw new Error("Couldn't register this device."); } // Unsubscribe locally and remove the endpoint server-side. export async function disable() { if (!supported()) return; const reg = await registration(); const sub = await reg.pushManager.getSubscription(); if (!sub) return; try { await apiFetch("api/v2/push/subscribe", { method: "DELETE", json: { endpoint: sub.endpoint } }); } catch (e) { /* best-effort server cleanup */ } await sub.unsubscribe(); } // Ask the server to push a test notification to every device on the account. export async function sendTest() { const res = await apiFetch("api/v2/push/test", { method: "POST" }); if (!res.ok) throw new Error("Couldn't send a test notification."); return res.json(); }