"use client";
import { useEffect, useMemo, useState } from "react";
import { Bell, BellRing, Clock3, 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 { useMeteoStationPositions } from "@/hooks/use-meteo-stations";
import { useWeatherStations } from "@/hooks/use-weather-stations";
import { DEFAULT_STATION_ID } from "@/lib/constants";
import { useI18n } from "@/lib/i18n";
import { locateSynopStations } from "@/lib/location-utils";
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 isAppleMobilePlatform() {
if (typeof window === "undefined") return false;
const userAgent = window.navigator.userAgent;
const platform = window.navigator.platform;
const isIPadOSDesktopMode = platform === "MacIntel" && window.navigator.maxTouchPoints > 1;
return /iPad|iPhone|iPod/.test(userAgent) || isIPadOSDesktopMode;
}
function getNotificationSupportStatus(): NotificationSupportStatus {
if (typeof window === "undefined") return "checking";
if (!("Notification" in window) || !("serviceWorker" in navigator) || !("PushManager" in window)) return "unsupported";
if (isAppleMobilePlatform() && !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 (
);
}
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 { data: stations } = useWeatherStations();
const { data: positions = [] } = useMeteoStationPositions();
const selectedStationId = useWeatherStore((state) => state.selectedStationId);
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
const notificationsEnabled = useWeatherStore((state) => state.warningNotificationsEnabled);
const morningBriefEnabled = useWeatherStore((state) => state.morningBriefNotificationsEnabled);
const tomorrowBriefEnabled = useWeatherStore((state) => state.tomorrowBriefNotificationsEnabled);
const provinceMode = useWeatherStore((state) => state.warningNotificationProvinceMode);
const manualProvince = useWeatherStore((state) => state.warningNotificationProvince);
const setNotificationsEnabled = useWeatherStore((state) => state.setWarningNotificationsEnabled);
const setMorningBriefEnabled = useWeatherStore((state) => state.setMorningBriefNotificationsEnabled);
const setTomorrowBriefEnabled = useWeatherStore((state) => state.setTomorrowBriefNotificationsEnabled);
const setProvinceMode = useWeatherStore((state) => state.setWarningNotificationProvinceMode);
const setManualProvince = useWeatherStore((state) => state.setWarningNotificationProvince);
const selectedStation = stations?.find((station) => station.id === selectedStationId)
?? stations?.find((station) => station.id === DEFAULT_STATION_ID)
?? stations?.[0];
const activeLocation = selectedLocation?.stationId === selectedStation?.id ? selectedLocation : null;
const stationPosition = selectedStation
? locateSynopStations(stations ?? [], positions).find((station) => station.id === selectedStation.id)
: null;
const notificationLatitude = Number.isFinite(activeLocation?.latitude) ? activeLocation?.latitude : stationPosition?.latitude ?? null;
const notificationLongitude = Number.isFinite(activeLocation?.longitude) ? activeLocation?.longitude : stationPosition?.longitude ?? null;
const notificationLocationName = activeLocation?.name ?? selectedStation?.name ?? null;
const notificationCountyTeryt = activeLocation?.countyTeryt ?? null;
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, {
morningBriefEnabled,
tomorrowBriefEnabled,
latitude: notificationLatitude,
longitude: notificationLongitude,
locationName: notificationLocationName,
countyTeryt: notificationCountyTeryt,
}).catch(() => undefined);
}
syncSubscriptionProvince();
return () => {
cancelled = true;
};
}, [effectiveProvince, language, morningBriefEnabled, notificationCountyTeryt, notificationLatitude, notificationLocationName, notificationLongitude, notificationStatus, notificationsEnabled, tomorrowBriefEnabled, 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, {
morningBriefEnabled,
tomorrowBriefEnabled,
latitude: notificationLatitude,
longitude: notificationLongitude,
locationName: notificationLocationName,
countyTeryt: notificationCountyTeryt,
});
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);
setMorningBriefEnabled(false);
setTomorrowBriefEnabled(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);
}
}
async function updateMorningBriefPreference(enabled: boolean) {
setMorningBriefEnabled(enabled);
if (!notificationsEnabled || !effectiveProvince || notificationStatus !== "granted") return;
try {
const registration = await navigator.serviceWorker?.getRegistration();
const subscription = await registration?.pushManager.getSubscription();
if (!subscription) return;
await savePushSubscription(subscription, effectiveProvince, language, {
morningBriefEnabled: enabled,
tomorrowBriefEnabled,
latitude: notificationLatitude,
longitude: notificationLongitude,
locationName: notificationLocationName,
countyTeryt: notificationCountyTeryt,
});
} catch {
setNotificationMessage(t("settings.notifications.saveError"));
}
}
async function updateTomorrowBriefPreference(enabled: boolean) {
setTomorrowBriefEnabled(enabled);
if (!notificationsEnabled || !effectiveProvince || notificationStatus !== "granted") return;
try {
const registration = await navigator.serviceWorker?.getRegistration();
const subscription = await registration?.pushManager.getSubscription();
if (!subscription) return;
await savePushSubscription(subscription, effectiveProvince, language, {
morningBriefEnabled,
tomorrowBriefEnabled: enabled,
latitude: notificationLatitude,
longitude: notificationLongitude,
locationName: notificationLocationName,
countyTeryt: notificationCountyTeryt,
});
} catch {
setNotificationMessage(t("settings.notifications.saveError"));
}
}
return (
{t("settings.section")}
{t("settings.title")}
{t("settings.description")}
IMGW
{t("settings.notifications.title")}
{t("settings.notifications.description")}
{t("settings.notifications.deviceTitle")}
{notificationStatusLabel}
{t("settings.notifications.enable")}
{t("settings.notifications.enableDescription")}
{notificationMessage &&
{notificationMessage}
}
{t("settings.notifications.implementationNote")}
);
}