"use client"; import { useEffect, useState, useSyncExternalStore } from "react"; import { Button } from "@/components/ui"; import { haptic } from "@/lib/haptics"; import { localeHeader, useI18n } from "@/lib/i18n"; import { authHeaders, saveSettings, settingsStore } from "@/lib/settings"; const GOALS = [10, 20, 30, 50]; type PushStatus = { enabled: boolean; publicKey?: string }; 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 bytes = new Uint8Array(raw.length); for (let index = 0; index < raw.length; index++) bytes[index] = raw.charCodeAt(index); return bytes; } /** * Settings → Learning: the daily goal, plus push reminders — but only when * the server has VAPID keys configured. Without them the reminder block is * hidden completely, exactly as if the feature did not exist. */ export function LearningSection() { const { t } = useI18n(); const dailyGoal = useSyncExternalStore( settingsStore.subscribe, () => settingsStore.getSnapshot().dailyGoal, () => 20, ); const [status, setStatus] = useState(null); const [subscribed, setSubscribed] = useState(false); const [endpoint, setEndpoint] = useState(null); const [busy, setBusy] = useState(false); const [notice, setNotice] = useState(null); const pushSupported = typeof window !== "undefined" && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window; useEffect(() => { if (!pushSupported) return; let cancelled = false; void (async () => { try { const response = await fetch("/api/push", { headers: { ...authHeaders(), ...localeHeader() }, cache: "no-store", }); const body = (await response.json()) as PushStatus; if (cancelled) return; setStatus(body); if (body.enabled) { const registration = await navigator.serviceWorker.ready.catch(() => null); const subscription = await registration?.pushManager.getSubscription(); if (!cancelled && subscription) { setSubscribed(true); setEndpoint(subscription.endpoint); } } } catch { if (!cancelled) setStatus({ enabled: false }); } })(); return () => { cancelled = true; }; }, [pushSupported]); const enable = async () => { if (!status?.publicKey) return; setBusy(true); setNotice(null); try { const permission = await Notification.requestPermission(); if (permission !== "granted") { setNotice(t("settings.reminderDenied")); return; } const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(status.publicKey) as unknown as BufferSource, }); const response = await fetch("/api/push", { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders(), ...localeHeader() }, body: JSON.stringify(subscription.toJSON()), }); if (!response.ok) throw new Error("subscribe failed"); setSubscribed(true); setEndpoint(subscription.endpoint); setNotice(t("settings.reminderOn")); haptic("toggle"); } catch { setNotice(t("settings.reminderError")); } finally { setBusy(false); } }; const disable = async () => { setBusy(true); setNotice(null); try { const registration = await navigator.serviceWorker.ready.catch(() => null); const subscription = await registration?.pushManager.getSubscription(); if (subscription) { await fetch(`/api/push?endpoint=${encodeURIComponent(subscription.endpoint)}`, { method: "DELETE", headers: { ...authHeaders(), ...localeHeader() }, }); await subscription.unsubscribe(); } setSubscribed(false); setEndpoint(null); setNotice(t("settings.reminderOff")); } catch { setNotice(t("settings.reminderError")); } finally { setBusy(false); } }; const test = async () => { if (!endpoint) return; setBusy(true); setNotice(null); try { const response = await fetch("/api/push/test", { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders(), ...localeHeader() }, body: JSON.stringify({ endpoint }), }); if (!response.ok) throw new Error("test failed"); setNotice(t("settings.reminderOn")); } catch { setNotice(t("settings.reminderError")); } finally { setBusy(false); } }; const iosNeedsInstall = typeof navigator !== "undefined" && /iPhone|iPad|iPod/.test(navigator.userAgent) && !(navigator as Navigator & { standalone?: boolean }).standalone; return (

{t("settings.learning")}

{t("settings.dailyGoal")}

{t("settings.dailyGoalNote")}

{GOALS.map((goal) => ( ))}
{pushSupported && status?.enabled ? (

{t("settings.reminders")}

{t("settings.remindersNote")}

{iosNeedsInstall ? (

{t("settings.remindersIos")}

) : null}
{subscribed ? ( <> ) : ( )}
{notice ?

{notice}

: null}
) : null}
); }