/** * Push notification subscription (Session 27). * * Client-side helpers to request permission and (un)subscribe to Web Push * via the active service worker. The SW already handles incoming `push` * and `notificationclick` events (see web/src/sw.ts); these helpers manage * the browser→push-service subscription handshake. * * VAPID: subscription requires NEXT_PUBLIC_VAPID_PUBLIC_KEY. Until that * key is generated and set (a future session), subscribeToPush returns * null rather than throwing — callers degrade gracefully. * * The resulting PushSubscription is meant to be POSTed to the backend and * stored (Supabase) so the notification trigger system can target it. */ /** Browser support check — both SW and PushManager must exist. */ export function isPushSupported(): boolean { return ( typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window ); } /** * VAPID public keys are base64url; the subscribe API wants a BufferSource. * We return the backing ArrayBuffer so the type is unambiguously * ArrayBuffer (not ArrayBufferLike) for `applicationServerKey`. */ function vapidKeyToBuffer(base64String: string): ArrayBuffer { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const raw = window.atob(base64); const buffer = new ArrayBuffer(raw.length); const view = new Uint8Array(buffer); for (let i = 0; i < raw.length; i += 1) view[i] = raw.charCodeAt(i); return buffer; } /** * Request permission and subscribe. Returns the PushSubscription on * success, or null when unsupported / denied / VAPID key missing. */ export async function subscribeToPush(): Promise { if (!isPushSupported()) return null; const vapidKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY; if (!vapidKey) { console.warn('[push] NEXT_PUBLIC_VAPID_PUBLIC_KEY not set — cannot subscribe yet.'); return null; } const permission = await Notification.requestPermission(); if (permission !== 'granted') return null; const registration = await navigator.serviceWorker.ready; // Reuse an existing subscription if the browser already has one. const existing = await registration.pushManager.getSubscription(); if (existing) return existing; return registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidKeyToBuffer(vapidKey), }); } /** Unsubscribe the active push subscription, if any. Idempotent. */ export async function unsubscribeFromPush(): Promise { if (!isPushSupported()) return true; const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); if (!subscription) return true; return subscription.unsubscribe(); } /** Current permission state without prompting — for UI affordances. */ export function pushPermission(): NotificationPermission | 'unsupported' { if (!isPushSupported()) return 'unsupported'; return Notification.permission; }