function urlBase64ToUint8Array(base64String: string) { const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } export function isPushSupported(): boolean { return "serviceWorker" in navigator && "PushManager" in window; } export async function getExistingSubscription(): Promise { if (!isPushSupported()) return null; try { const reg = await navigator.serviceWorker.getRegistration("/sw.js"); if (reg) return await reg.pushManager.getSubscription(); } catch {} return null; } export async function subscribeToPush(): Promise { const keyRes = await fetch("/api/push/vapid-key"); const { publicKey } = await keyRes.json(); if (!publicKey) throw new Error("VAPID key not configured"); const reg = await navigator.serviceWorker.register("/sw.js"); await navigator.serviceWorker.ready; const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey), }); const subJson = sub.toJSON(); const res = await fetch("/api/push/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: subJson.endpoint, keys: subJson.keys }), }); if (!res.ok) throw new Error("Subscription save failed"); return true; } export async function unsubscribeFromPush(): Promise { const reg = await navigator.serviceWorker.getRegistration("/sw.js"); if (reg) { const sub = await reg.pushManager.getSubscription(); if (sub) { const res = await fetch("/api/push/unsubscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: sub.endpoint }), }); if (!res.ok) throw new Error("Unsubscribe failed"); await sub.unsubscribe(); } } return true; }