"use client"; import { AnimatePresence, motion } from "motion/react"; import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuArrowRight, LuBriefcase, LuCookie, LuFolders, LuGithub, LuGlobe, LuHeart, LuLoaderCircle, LuMic, LuNetwork, LuShieldCheck, LuTerminal, LuTriangleAlert, LuUsers, } from "react-icons/lu"; import { Logo } from "@/components/icons/logo"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { useBrowserSetup } from "@/hooks/use-browser-setup"; import { usePermissions } from "@/hooks/use-permissions"; import { getBrowserDisplayName } from "@/lib/browser-utils"; type WelcomeStep = "intro" | "license" | "permissions" | "setup"; const panelTransition = { type: "spring", stiffness: 260, damping: 28, } as const; const panelVariants = { enter: { opacity: 0, y: 12 }, center: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -12 }, }; // Concrete feature list shown on the intro step, rendered as an icon grid. const FEATURES = [ { key: "welcome.features.items.setDefault", Icon: LuGlobe }, { key: "welcome.features.items.proxy", Icon: LuNetwork }, { key: "welcome.features.items.vpn", Icon: LuShieldCheck }, { key: "welcome.features.items.profiles", Icon: LuUsers }, { key: "welcome.features.items.api", Icon: LuTerminal }, { key: "welcome.features.items.openSource", Icon: LuGithub }, { key: "welcome.features.items.groups", Icon: LuFolders }, { key: "welcome.features.items.cookies", Icon: LuCookie }, ] as const; function formatBytes(bytes: number): string { if (!(bytes > 0)) return "0 B"; const units = ["B", "KB", "MB", "GB"]; const exponent = Math.min( units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)), ); const value = bytes / 1024 ** exponent; const rounded = exponent === 0 ? value : Math.round(value * 10) / 10; return `${rounded} ${units[exponent]}`; } function formatDuration(seconds: number): string { const total = Math.max(0, Math.round(seconds)); if (total < 60) return `${total}s`; const minutes = Math.floor(total / 60); const remainder = total % 60; return `${minutes}m ${String(remainder).padStart(2, "0")}s`; } export function WelcomeDialog({ isOpen, needsSetup, onComplete, }: { isOpen: boolean; /** * Whether this user still needs the browser-download + profile-creation flow. * False when they already have a profile — then the welcome and commercial-use * steps still show, but "continue" finishes onboarding instead of proceeding * to permissions/download. */ needsSetup: boolean; onComplete: () => void; }) { const { t } = useTranslation(); const { requestPermission } = usePermissions(); const [step, setStep] = useState("intro"); // Where the "skip" / "continue" affordances go: into the setup flow when a // browser/profile is still needed, otherwise straight to completion. const advanceToSetup = () => { if (needsSetup) setStep("setup"); else onComplete(); }; const [requesting, setRequesting] = useState(false); // Track the required browser's download + extraction the whole time the // dialog is open, so progress is live by the time the user reaches setup. const setup = useBrowserSetup("wayfern", isOpen); const browserName = getBrowserDisplayName("wayfern"); const requestPermissions = useCallback(async () => { setRequesting(true); try { await requestPermission("microphone"); await requestPermission("camera"); } catch (err) { console.error("Permission request failed:", err); } finally { setRequesting(false); setStep("setup"); } }, [requestPermission]); return ( {}}> {t("welcome.title")} {step === "intro" && (

{t("welcome.title")}

{t("welcome.tagline")}

{t("welcome.features.title")}

{FEATURES.map(({ key, Icon }, i) => (
{t(key)}
))}
)} {step === "license" && (

{t("welcome.license.title")}

{t("welcome.license.body")}

{t("welcome.license.personalTitle")}
{t("welcome.license.personalDesc")}
{t("welcome.license.commercialTitle")} {t("welcome.license.trialBadge")}
{t("welcome.license.commercialDesc")}
)} {step === "permissions" && (

{t("welcome.permissions.title")}

{t("welcome.permissions.desc")}

)} {step === "setup" && ( {setup.phase === "error" ? ( <>

{t("welcome.ready.errorTitle")}

{setup.error?.stage === "downloading" ? t("welcome.ready.errorDownload", { browser: browserName, }) : setup.error?.stage === "extracting" || setup.error?.stage === "verifying" ? t("welcome.ready.errorExtraction", { browser: browserName, }) : t("welcome.ready.errorGeneric", { browser: browserName, })}

{/* No escape hatch here: a browser must finish downloading before onboarding can complete, so the only action on failure is to retry. */} ) : ( <>

{t("welcome.ready.title")}

{setup.phase === "ready" ? t("welcome.ready.descReady") : setup.phase === "extracting" ? t("welcome.ready.descExtracting") : t("welcome.ready.descDownloading")}

{setup.phase === "downloading" && (
{t("welcome.ready.downloading")} {setup.downloadPercent}%
{setup.totalBytes != null ? t("welcome.ready.stats", { downloaded: formatBytes(setup.downloadedBytes), total: formatBytes(setup.totalBytes), }) : formatBytes(setup.downloadedBytes)} {setup.speedBytesPerSec > 0 && ( {t("welcome.ready.speed", { speed: formatBytes(setup.speedBytesPerSec), })} )} {setup.etaSeconds != null && Number.isFinite(setup.etaSeconds) && setup.etaSeconds > 0 && ( {t("welcome.ready.timeLeft", { time: formatDuration(setup.etaSeconds), })} )}
)} {setup.phase === "extracting" && (
{setup.extractionOvertime ? (
{t("welcome.ready.almostFinished")}
) : ( <>
{t("welcome.ready.extracting")} {setup.extractionPercent}%
)}
)} {setup.phase === "ready" && ( )} )}
)}
); }