diff --git a/client/src/lib/push-utils.ts b/client/src/lib/push-utils.ts new file mode 100644 index 0000000..04bf346 --- /dev/null +++ b/client/src/lib/push-utils.ts @@ -0,0 +1,63 @@ +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; +}