"use client" import { useCallback, useEffect, useRef, useState } from "react" import toast from "react-hot-toast" import { CheckCircle2, Loader2, MessageCircle, Timer } from "lucide-react" import { MAX_STOREFRONT_OTP_SESSION_MINUTES, MIN_STOREFRONT_OTP_SESSION_MINUTES, } from "@/lib/storefront-chat/otpCrypto" const MAX_HOURS = Math.floor(MAX_STOREFRONT_OTP_SESSION_MINUTES / 60) function splitTotalMinutes(total: number): { hours: number; minutes: number } { const h = Math.floor(total / 60) const m = total % 60 return { hours: h, minutes: m } } export default function StorefrontSettingsPage() { const [activeStoreId, setActiveStoreId] = useState(null) const [hoursDraft, setHoursDraft] = useState("1") const [minutesDraft, setMinutesDraft] = useState("0") const [saving, setSaving] = useState(false) const [savedFlash, setSavedFlash] = useState(false) const flashTimerRef = useRef | null>(null) const loadStore = useCallback(async () => { try { const res = await fetch("/api/stores") if (!res.ok) return const data = await res.json() const active = data.stores?.find((s: { isActive: boolean }) => s.isActive) as | { id?: string } | undefined if (!active?.id) { setActiveStoreId(null) return } setActiveStoreId(active.id) const settingsRes = await fetch(`/api/stores/${active.id}`, { cache: "no-store" }) if (!settingsRes.ok) return const s = (await settingsRes.json()) as { storefrontOtpSessionMinutes?: number } const total = typeof s.storefrontOtpSessionMinutes === "number" ? s.storefrontOtpSessionMinutes : 60 const { hours, minutes } = splitTotalMinutes(total) setHoursDraft(String(hours)) setMinutesDraft(String(minutes)) } catch { // ignore } }, []) useEffect(() => { void loadStore() }, [loadStore]) useEffect(() => { return () => { if (flashTimerRef.current) clearTimeout(flashTimerRef.current) } }, []) const save = async () => { if (!activeStoreId) { toast.error("No active store") return } const hTrim = hoursDraft.trim() const mTrim = minutesDraft.trim() const h = hTrim === "" ? 0 : Number.parseInt(hTrim, 10) const m = mTrim === "" ? 0 : Number.parseInt(mTrim, 10) if (!Number.isFinite(h) || h < 0 || h > MAX_HOURS || !Number.isFinite(m) || m < 0 || m > 59) { toast.error(`Hours: 0–${MAX_HOURS}, minutes: 0–59.`) return } if (h === MAX_HOURS && m > 0) { toast.error(`Maximum session is ${MAX_HOURS} hours (10080 minutes).`) return } const totalMinutes = h * 60 + m if (totalMinutes < MIN_STOREFRONT_OTP_SESSION_MINUTES) { toast.error(`Session must be at least ${MIN_STOREFRONT_OTP_SESSION_MINUTES} minutes total.`) return } if (totalMinutes > MAX_STOREFRONT_OTP_SESSION_MINUTES) { toast.error(`Session cannot exceed ${MAX_STOREFRONT_OTP_SESSION_MINUTES} minutes (one week).`) return } setSaving(true) try { const res = await fetch(`/api/stores/${activeStoreId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ storefrontOtpSessionMinutes: totalMinutes }), }) const data = (await res.json().catch(() => ({}))) as { message?: string } if (!res.ok) throw new Error(data.message || "Save failed") if (flashTimerRef.current) { clearTimeout(flashTimerRef.current) flashTimerRef.current = null } setSavedFlash(true) flashTimerRef.current = setTimeout(() => { setSavedFlash(false) flashTimerRef.current = null }, 2800) toast.success("Storefront session length saved.") } catch (e) { toast.error(e instanceof Error ? e.message : "Could not save") } finally { setSaving(false) } } return (

Storefront chat (OTP session)

After a customer verifies their email with a one-time code, the widget stays signed in for this length. The code itself still expires in 10 minutes; this only affects the session after verification.

setHoursDraft(e.target.value.replace(/[^\d]/g, ""))} className="w-24 px-4 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder:text-slate-500 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500/30 focus:border-indigo-500/50" aria-describedby="otp-session-hint" />
{ const raw = e.target.value.replace(/[^\d]/g, "").slice(0, 2) if (raw === "") { setMinutesDraft("") return } const n = Number.parseInt(raw, 10) setMinutesDraft(String(Math.min(59, n))) }} className="w-24 px-4 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder:text-slate-500 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-indigo-500/30 focus:border-indigo-500/50" aria-describedby="otp-session-hint" />

Total must be between {MIN_STOREFRONT_OTP_SESSION_MINUTES} and{" "} {MAX_STOREFRONT_OTP_SESSION_MINUTES} minutes (up to{" "} {MAX_HOURS} hours). Default was 1 hour.

) }