95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
// Notificaciones: toasts dentro de la app + soporte de Web Push nativo
|
|
// mediante el Service Worker de mensajería en /sw.js.
|
|
import { toast } from "sonner";
|
|
|
|
// Clave pública VAPID (configurable con VITE_VAPID_KEY).
|
|
export const VAPID_KEY =
|
|
import.meta.env["VITE_VAPID_KEY"] ?? "wLVf8PBUOuPW8Zn5l_ksTTiaf9wb3OSSDaJAuxKPru0";
|
|
|
|
/** Alias retrocompatible. */
|
|
export const PUBLIC_VAPID_KEY = VAPID_KEY;
|
|
|
|
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
const raw = atob(base64);
|
|
const salida = new Uint8Array(raw.length);
|
|
for (let i = 0; i < raw.length; i += 1) salida[i] = raw.charCodeAt(i);
|
|
return salida;
|
|
}
|
|
|
|
export async function registrarServiceWorker(): Promise<ServiceWorkerRegistration | null> {
|
|
if (typeof window === "undefined" || !("serviceWorker" in navigator)) return null;
|
|
try {
|
|
return await navigator.serviceWorker.register("/sw.js", { scope: "/" });
|
|
} catch (error) {
|
|
console.warn("No se pudo registrar el Service Worker de notificaciones", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function pedirPermisoNotificaciones(): Promise<NotificationPermission | "unsupported"> {
|
|
if (typeof window === "undefined" || !("Notification" in window)) return "unsupported";
|
|
if (Notification.permission === "granted") return "granted";
|
|
try {
|
|
return await Notification.requestPermission();
|
|
} catch {
|
|
return "denied";
|
|
}
|
|
}
|
|
|
|
export type ResultadoPush =
|
|
| { ok: true; suscripcion: PushSubscription }
|
|
| { ok: false; motivo: "unsupported" | "denied" | "sin-clave" | "error" };
|
|
|
|
export async function suscribirNotificacionesPush(): Promise<ResultadoPush> {
|
|
if (typeof window === "undefined" || !("serviceWorker" in navigator) || !("PushManager" in window)) {
|
|
return { ok: false, motivo: "unsupported" };
|
|
}
|
|
|
|
const permiso = await pedirPermisoNotificaciones();
|
|
if (permiso !== "granted") return { ok: false, motivo: "denied" };
|
|
|
|
if (!VAPID_KEY || VAPID_KEY.startsWith("REEMPLAZA")) {
|
|
return { ok: false, motivo: "sin-clave" };
|
|
}
|
|
|
|
try {
|
|
const registration = (await navigator.serviceWorker.getRegistration("/sw.js")) ?? (await registrarServiceWorker());
|
|
if (!registration) return { ok: false, motivo: "error" };
|
|
const activo = await navigator.serviceWorker.ready;
|
|
|
|
const existente = await activo.pushManager.getSubscription();
|
|
const suscripcion =
|
|
existente ??
|
|
(await activo.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array(VAPID_KEY) as BufferSource,
|
|
}));
|
|
|
|
console.log("Push Subscription JSON:", JSON.stringify(suscripcion));
|
|
|
|
await fetch("/api/guardar-suscripcion", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify(suscripcion.toJSON()),
|
|
});
|
|
|
|
return { ok: true, suscripcion };
|
|
} catch (error) {
|
|
console.warn("No se pudo suscribir a notificaciones push", error);
|
|
return { ok: false, motivo: "error" };
|
|
}
|
|
}
|
|
|
|
export function notificar(titulo: string, cuerpo: string) {
|
|
toast.success(titulo, { description: cuerpo });
|
|
if (typeof window !== "undefined" && "Notification" in window && Notification.permission === "granted") {
|
|
try {
|
|
new Notification(titulo, { body: cuerpo, icon: "/icons/icon-192.png" });
|
|
} catch {
|
|
/* silencioso: algunos navegadores exigen Service Worker */
|
|
}
|
|
}
|
|
}
|