import { BellOff, BellRing, Loader } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { writeFavorites } from "~/utils/idb"; /** Convert a base64url string (as returned by the VAPID endpoint) to a Uint8Array. */ function urlBase64ToUint8Array(base64String: string): Uint8Array { const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); const rawData = atob(base64); return Uint8Array.from([...rawData].map((c) => c.charCodeAt(0))); } /** Sync all three favourites lists from localStorage to IndexedDB. */ async function syncFavouritesToIdb() { const keys = [ "favouriteStops", "favouriteRoutes", "favouriteAgencies", ] as const; await Promise.all( keys.map((key) => { const raw = localStorage.getItem(key); const ids: string[] = raw ? JSON.parse(raw) : []; return writeFavorites(key, ids); }) ); } type Status = | "loading" // checking current state | "unsupported" // browser does not support Push API | "denied" // permission was explicitly blocked | "subscribed" // user is actively subscribed | "unsubscribed"; // user is not subscribed (or permission not yet granted) export function PushNotificationSettings() { const { t } = useTranslation(); const [status, setStatus] = useState("loading"); const [working, setWorking] = useState(false); useEffect(() => { checkStatus().then(setStatus); }, []); async function checkStatus(): Promise { if (!("serviceWorker" in navigator) || !("PushManager" in window)) { return "unsupported"; } if (Notification.permission === "denied") return "denied"; try { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); return sub ? "subscribed" : "unsubscribed"; } catch { return "unsubscribed"; } } async function subscribe() { setWorking(true); try { const permission = await Notification.requestPermission(); if (permission !== "granted") { setStatus("denied"); return; } // Fetch the VAPID public key const res = await fetch("/api/push/vapid-public-key"); if (!res.ok) { console.error("Push notifications not configured on this server."); return; } const { publicKey } = await res.json(); const reg = await navigator.serviceWorker.ready; const subscription = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey), }); // Mirror favourites to IDB before registering so the SW has them from day 1 await syncFavouritesToIdb(); const json = subscription.toJSON(); await fetch("/api/push/subscribe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: json.endpoint, p256Dh: json.keys?.p256dh, auth: json.keys?.auth, }), }); setStatus("subscribed"); } finally { setWorking(false); } } async function unsubscribe() { setWorking(true); try { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub) { const endpoint = sub.endpoint; await sub.unsubscribe(); await fetch("/api/push/unsubscribe", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint }), }); } setStatus("unsubscribed"); } finally { setWorking(false); } } return (

{t("settings.push_title", "Notificaciones")}

{t( "settings.push_description", "Recibe notificaciones cuando haya alertas de servicio relevantes para tus paradas, líneas o operadores favoritos." )}

{status === "loading" && (
{t("common.loading", "Cargando...")}
)} {status === "unsupported" && (

{t( "settings.push_unsupported", "Tu navegador no soporta notificaciones push. Prueba con Chrome, Edge o Firefox." )}

)} {status === "denied" && (

{t( "settings.push_permission_denied", "Has bloqueado los permisos de notificación en este navegador. Para activarlos, ve a la configuración del sitio y permite las notificaciones." )}

)} {(status === "subscribed" || status === "unsubscribed") && ( )}
); }