// Push-varsler til telefonens OS (Web Push/VAPID, 2026-07-28). Ren // klientkode -- selve abonnementet lagres server-side via /push/subscribe. // VIKTIG plattformbegrensning (se FEATURE_BACKLOG.md): iOS Safari støtter // Web Push KUN når PWA-en er lagt til på hjemskjermen (installert som // "standalone") -- en vanlig Safari-fane kan aldri abonnere, uansett // tillatelse. isPushSupported() dekker dette. function urlBase64ToUint8Array(base64: string): Uint8Array { const padding = "=".repeat((4 - (base64.length % 4)) % 4) const base64Safe = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/") const raw = atob(base64Safe) const output = new Uint8Array(raw.length) for (let i = 0; i < raw.length; i++) output[i] = raw.charCodeAt(i) return output } export function isPushSupported(): boolean { if (typeof window === "undefined") return false if (!("serviceWorker" in navigator) || !("PushManager" in window) || !("Notification" in window)) return false const nav = window.navigator as Navigator & { standalone?: boolean } const isIOS = /iphone|ipad|ipod/i.test(window.navigator.userAgent) const isStandalone = window.matchMedia("(display-mode: standalone)").matches || nav.standalone === true // iOS krever installasjon til hjemskjermen -- alle andre støttede // nettlesere (Chrome/Edge/Firefox-familien) trenger det ikke. return !isIOS || isStandalone } export async function getExistingSubscription(): Promise { if (!("serviceWorker" in navigator)) return null const registration = await navigator.serviceWorker.ready return registration.pushManager.getSubscription() } export async function subscribeToPush(): Promise<{ ok: boolean; reason?: string }> { if (!isPushSupported()) { return { ok: false, reason: "unsupported" } } const permission = await Notification.requestPermission() if (permission !== "granted") { return { ok: false, reason: "denied" } } const keyRes = await fetch("/push/vapid-public-key", { credentials: "include" }) if (!keyRes.ok) return { ok: false, reason: "server-error" } const { public_key, configured }: { public_key: string | null; configured: boolean } = await keyRes.json() if (!configured || !public_key) return { ok: false, reason: "not-configured" } const registration = await navigator.serviceWorker.ready const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(public_key), }) const json = subscription.toJSON() const subscribeRes = await fetch("/push/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ endpoint: json.endpoint, keys: { p256dh: json.keys?.p256dh, auth: json.keys?.auth }, }), }) if (!subscribeRes.ok) { return { ok: false, reason: "server-error" } } return { ok: true } } export async function unsubscribeFromPush(): Promise { const subscription = await getExistingSubscription() if (!subscription) return const endpoint = subscription.endpoint await subscription.unsubscribe() await fetch("/push/subscribe", { method: "DELETE", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ endpoint }), }).catch(() => {}) }