"use client"; import { useEffect, useMemo, useState } from "react"; import { Bell, BellRing, Languages, MapPinned, Palette, Settings, Smartphone } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { LanguageToggle } from "@/components/ui/language-toggle"; import { ThemeToggle } from "@/components/ui/theme-toggle"; import { DEFAULT_STATION_ID } from "@/lib/constants"; import { useI18n } from "@/lib/i18n"; import { decodeBase64UrlKey, deletePushSubscription, fetchVapidPublicKey, savePushSubscription, sendTestPushNotification } from "@/lib/notification-api"; import { formatProvinceName, getProvinceForSelection, PROVINCES } from "@/lib/provinces"; import { useWeatherStore } from "@/lib/store"; import type { Province } from "@/types/province"; type NotificationSupportStatus = "checking" | "unconfigured" | "unsupported" | "needs-install" | "default" | "denied" | "granted"; function isStandaloneApp() { if (typeof window === "undefined") return false; const navigatorWithStandalone = window.navigator as Navigator & { standalone?: boolean }; return window.matchMedia("(display-mode: standalone)").matches || navigatorWithStandalone.standalone === true; } function getNotificationSupportStatus(): NotificationSupportStatus { if (typeof window === "undefined") return "checking"; if (!("Notification" in window) || !("serviceWorker" in navigator) || !("PushManager" in window)) return "unsupported"; if (!isStandaloneApp()) return "needs-install"; return Notification.permission; } function SettingsRow({ icon: Icon, title, description, children }: { icon: typeof Settings; title: string; description: string; children: React.ReactNode }) { return (

{title}

{description}

{children}
); } export function SettingsPage() { const { language, t } = useI18n(); const [notificationStatus, setNotificationStatus] = useState("checking"); const [vapidPublicKey, setVapidPublicKey] = useState(null); const [isSavingSubscription, setIsSavingSubscription] = useState(false); const [isSendingTestNotification, setIsSendingTestNotification] = useState(false); const [notificationMessage, setNotificationMessage] = useState(null); const selectedStationId = useWeatherStore((state) => state.selectedStationId); const selectedLocation = useWeatherStore((state) => state.selectedLocation); const notificationsEnabled = useWeatherStore((state) => state.warningNotificationsEnabled); const provinceMode = useWeatherStore((state) => state.warningNotificationProvinceMode); const manualProvince = useWeatherStore((state) => state.warningNotificationProvince); const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled); const setProvinceMode = useWeatherStore((state) => state.setWarningNotificationProvinceMode); const setManualProvince = useWeatherStore((state) => state.setWarningNotificationProvince); const selectedProvince = getProvinceForSelection(selectedLocation?.province, selectedStationId ?? DEFAULT_STATION_ID); const effectiveProvince = provinceMode === "manual" ? manualProvince : selectedProvince; const effectiveProvinceLabel = effectiveProvince ? formatProvinceName(effectiveProvince, language) : t("settings.notifications.noProvince"); useEffect(() => { const abortController = new AbortController(); const animationFrame = window.requestAnimationFrame(() => { setNotificationStatus(getNotificationSupportStatus()); fetchVapidPublicKey(abortController.signal) .then((config) => { setVapidPublicKey(config.publicKey); if (!config.configured) setNotificationStatus("unconfigured"); }) .catch(() => setNotificationStatus("unconfigured")); }); return () => { abortController.abort(); window.cancelAnimationFrame(animationFrame); }; }, []); useEffect(() => { if (!notificationsEnabled || !vapidPublicKey || !effectiveProvince || notificationStatus !== "granted") return; const province = effectiveProvince; let cancelled = false; async function syncSubscriptionProvince() { const registration = await navigator.serviceWorker?.getRegistration(); const subscription = await registration?.pushManager.getSubscription(); if (!subscription || cancelled) return; await savePushSubscription(subscription, province, language).catch(() => undefined); } syncSubscriptionProvince(); return () => { cancelled = true; }; }, [effectiveProvince, language, notificationStatus, notificationsEnabled, vapidPublicKey]); const notificationStatusLabel = useMemo(() => { switch (notificationStatus) { case "unconfigured": return t("settings.notifications.statusUnconfigured"); case "unsupported": return t("settings.notifications.statusUnsupported"); case "needs-install": return t("settings.notifications.statusNeedsInstall"); case "denied": return t("settings.notifications.statusDenied"); case "granted": return t("settings.notifications.statusGranted"); case "default": return t("settings.notifications.statusReady"); default: return t("settings.notifications.statusChecking"); } }, [notificationStatus, t]); const canUsePush = Boolean(vapidPublicKey && effectiveProvince && notificationStatus !== "unsupported" && notificationStatus !== "needs-install" && notificationStatus !== "denied" && notificationStatus !== "unconfigured"); async function getServiceWorkerRegistration() { if (!("serviceWorker" in navigator)) throw new Error("Service worker is not available."); return await navigator.serviceWorker.getRegistration() ?? await navigator.serviceWorker.register("/sw.js"); } async function enableWarningNotifications() { if (!vapidPublicKey || !effectiveProvince) { setNotificationMessage(t("settings.notifications.saveError")); return; } if (!("Notification" in window)) { setNotificationStatus("unsupported"); return; } setIsSavingSubscription(true); setNotificationMessage(null); try { const permission = Notification.permission === "granted" ? "granted" : await Notification.requestPermission(); setNotificationStatus(permission); if (permission !== "granted") { setNotificationsEnabled(false); return; } const registration = await getServiceWorkerRegistration(); const existingSubscription = await registration.pushManager.getSubscription(); const subscription = existingSubscription ?? await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: decodeBase64UrlKey(vapidPublicKey), }); await savePushSubscription(subscription, effectiveProvince, language); setNotificationsEnabled(true); setNotificationMessage(t("settings.notifications.saveSuccess")); } catch { setNotificationsEnabled(false); setNotificationMessage(t("settings.notifications.saveError")); } finally { setIsSavingSubscription(false); } } async function disableWarningNotifications() { setIsSavingSubscription(true); setNotificationMessage(null); try { const registration = await navigator.serviceWorker?.getRegistration(); const subscription = await registration?.pushManager.getSubscription(); if (subscription) { await deletePushSubscription(subscription.endpoint).catch(() => undefined); await subscription.unsubscribe(); } setNotificationsEnabled(false); setNotificationMessage(t("settings.notifications.disableSuccess")); } catch { setNotificationMessage(t("settings.notifications.saveError")); } finally { setIsSavingSubscription(false); } } async function sendTestNotification() { setIsSendingTestNotification(true); setNotificationMessage(null); try { const registration = await navigator.serviceWorker?.getRegistration(); const subscription = await registration?.pushManager.getSubscription(); if (!subscription) throw new Error("Push subscription is not available."); await sendTestPushNotification(subscription.endpoint); setNotificationMessage(t("settings.notifications.testSuccess")); } catch { setNotificationMessage(t("settings.notifications.testError")); } finally { setIsSendingTestNotification(false); } } return (

{t("settings.section")}

{t("settings.title")}

{t("settings.description")}

IMGW

{t("settings.notifications.title")}

{t("settings.notifications.description")}

{t("settings.notifications.enable")}

{t("settings.notifications.enableDescription")}

{notificationMessage &&

{notificationMessage}

}

{t("settings.notifications.implementationNote")}

); }