"use client"; // Offline notifications data layer (Web Push). Registers the service worker, subscribes the browser // with IIOS's VAPID key, and hands the subscription to the be-crm door so IIOS can reach this user // while no CRM tab is open. Push needs a real backend (VAPID key + delivery), so it is only offered // when the Shell is configured (live); demo mode reports it unsupported. // // Live contract (be-crm data door → IIOS /v1/notifications/*): // query crm.messenger.push.vapidKey {} -> { key } ('' = push disabled server-side) // cmd crm.messenger.push.subscribe { endpoint, keys, userAgent } -> { ok } // cmd crm.messenger.push.unsubscribe { endpoint } -> { ok } import { useCallback, useEffect, useState } from "react"; import { useAppShell } from "@abe-kap/appshell-sdk/react"; import { isShellConfigured } from "./appshell"; const SHELL = isShellConfigured(); const SW_URL = "/push-sw.js"; export type PushPermission = "default" | "granted" | "denied"; export interface PushState { /** Browser can do Web Push AND we have a live backend to deliver it. */ supported: boolean; /** OS/browser permission for notifications. */ permission: PushPermission; /** This browser currently has an active push subscription registered with the backend. */ subscribed: boolean; /** A subscribe/unsubscribe round-trip is in flight. */ busy: boolean; error: string | null; enable: () => Promise; disable: () => Promise; } /** VAPID keys travel as URL-safe base64; PushManager wants raw bytes. */ function urlBase64ToUint8Array(base64: string): Uint8Array { const padding = "=".repeat((4 - (base64.length % 4)) % 4); const normalized = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/"); const raw = atob(normalized); const out = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); return out; } function browserSupportsPush(): boolean { return typeof window !== "undefined" && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window; } /** Serialize a PushSubscription into the door's { endpoint, keys } shape. */ function toSubscribeBody(sub: PushSubscription): { endpoint: string; keys: { p256dh: string; auth: string }; userAgent: string } { const json = sub.toJSON(); return { endpoint: sub.endpoint, keys: { p256dh: json.keys?.p256dh ?? "", auth: json.keys?.auth ?? "" }, userAgent: typeof navigator !== "undefined" ? navigator.userAgent.slice(0, 400) : "", }; } export function usePushNotifications(): PushState { const { sdk } = useAppShell(); const [supported] = useState(() => SHELL && browserSupportsPush()); const [permission, setPermission] = useState("default"); const [subscribed, setSubscribed] = useState(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); // Reflect the current OS permission + whether a subscription already exists (e.g. across reloads). useEffect(() => { if (!supported) return; setPermission(Notification.permission as PushPermission); let cancelled = false; navigator.serviceWorker.ready .then((reg) => reg.pushManager.getSubscription()) .then((sub) => { if (!cancelled) setSubscribed(Boolean(sub)); }) .catch(() => {}); return () => { cancelled = true; }; }, [supported]); const enable = useCallback(async () => { if (!supported) return; setBusy(true); setError(null); try { const perm = await Notification.requestPermission(); setPermission(perm as PushPermission); if (perm !== "granted") throw new Error("Notifications permission was not granted."); const reg = await navigator.serviceWorker.register(SW_URL); await navigator.serviceWorker.ready; const { key } = await sdk.query<{ key: string }>("crm.messenger.push.vapidKey", {}); if (!key) throw new Error("Push is not enabled on the server (no VAPID key)."); const existing = await reg.pushManager.getSubscription(); const sub = existing ?? (await reg.pushManager.subscribe({ userVisibleOnly: true, // Cast: this lib's BufferSource type pins ArrayBuffer, but a plain Uint8Array is valid here. applicationServerKey: urlBase64ToUint8Array(key) as BufferSource, })); await sdk.command("crm.messenger.push.subscribe", toSubscribeBody(sub)); setSubscribed(true); } catch (e) { setError(e instanceof Error ? e.message : "Could not enable notifications."); } finally { setBusy(false); } }, [supported, sdk]); const disable = useCallback(async () => { if (!supported) return; setBusy(true); setError(null); try { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub) { // Tell the backend first (still has the endpoint), then drop the local subscription. await sdk.command("crm.messenger.push.unsubscribe", { endpoint: sub.endpoint }).catch(() => {}); await sub.unsubscribe(); } setSubscribed(false); } catch (e) { setError(e instanceof Error ? e.message : "Could not disable notifications."); } finally { setBusy(false); } }, [supported, sdk]); return { supported, permission, subscribed, busy, error, enable, disable }; }