// PWA glue: register the service worker, and (for signed-in users on a // push-enabled server) drive the notification opt-in toggle in the settings // panel. Anonymous visitors still get the offline reader — only the push // controls are gated behind sign-in + a configured VAPID key. (function () { if (!("serviceWorker" in navigator)) return; var CFG = window.PETE_PUSH || null; // { enabled, publicKey } or null var reg = null; navigator.serviceWorker.register("/sw.js").then(function (r) { reg = r; if (canPush()) initPushUI(); }).catch(function () {}); function canPush() { return !!(CFG && CFG.enabled && CFG.publicKey && window.PETE_USER && "PushManager" in window && "Notification" in window); } // ---- push subscription ---------------------------------------------------- function urlBase64ToUint8Array(base64String) { var padding = "=".repeat((4 - (base64String.length % 4)) % 4); var base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); var raw = atob(base64); var out = new Uint8Array(raw.length); for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); return out; } function currentSub() { if (!reg) return Promise.resolve(null); return reg.pushManager.getSubscription(); } function subscribe() { return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(CFG.publicKey), }).then(function (sub) { return fetch("/api/push/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(sub.toJSON()), credentials: "same-origin", }).then(function (res) { if (!res.ok) throw new Error("subscribe rejected"); return sub; }); }); } function unsubscribe() { return currentSub().then(function (sub) { if (!sub) return; var endpoint = sub.endpoint; return sub.unsubscribe().then(function () { return fetch("/api/push/unsubscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: endpoint }), credentials: "same-origin", }).catch(function () {}); }); }); } // ---- settings-panel toggle ------------------------------------------------ function initPushUI() { var slot = document.querySelector("[data-push-section]"); if (!slot) return; slot.hidden = false; var btn = slot.querySelector("[data-push-toggle]"); var note = slot.querySelector("[data-push-note]"); if (!btn) return; var busy = false; function paint(on, text) { btn.setAttribute("aria-pressed", on ? "true" : "false"); btn.textContent = on ? "Notifications on" : "Turn on notifications"; if (note && text != null) note.textContent = text; } function refresh() { if (Notification.permission === "denied") { btn.disabled = true; paint(false, "Notifications are blocked in your browser settings."); return; } currentSub().then(function (sub) { paint(!!sub, sub ? "You'll get a nudge when new stories land." : "Get a nudge when new stories land."); }); } btn.addEventListener("click", function () { if (busy) return; busy = true; btn.disabled = true; currentSub().then(function (sub) { if (sub) return unsubscribe().then(function () { paint(false, "Notifications off."); }); return Notification.requestPermission().then(function (perm) { if (perm !== "granted") { paint(false, "Permission denied."); return; } return subscribe().then(function () { paint(true, "You're all set — new stories will nudge you."); }); }); }).catch(function () { paint(false, "Something went wrong. Try again."); }).finally(function () { busy = false; btn.disabled = Notification.permission === "denied"; }); }); refresh(); } })();