"use client"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { ActivityList } from "@/components/mavi/activity-list"; import { NetworkSwitcher } from "@/components/mavi/network-switcher"; import { UnlockPanel } from "@/components/mavi/unlock-panel"; import { MaviWalletLogo } from "@/components/wallet/wallet-logos"; import { Badge } from "@/components/ui/badge"; import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; import { ErrorBanner } from "@/components/ui/error-banner"; import { addActivity, notifyActivityChanged, } from "@/lib/mavi/activity"; import { fetchEthBalance, fundFromAnvil } from "@/lib/mavi/chain"; import { getActiveNetwork } from "@/lib/mavi/network"; import { deleteVaultLocal, lockSession, readUnlockedSession, readVault, shortAddress, type UnlockedMavi, } from "@/lib/mavi/storage"; import { readLastLoginMethod, type LoginMethodId } from "@/lib/last-login"; import { cn } from "@/lib/utils"; export function MaviHome() { const router = useRouter(); const searchParams = useSearchParams(); const [mounted, setMounted] = useState(false); const [exists, setExists] = useState(false); const [address, setAddress] = useState(null); const [unlocked, setUnlocked] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [info, setInfo] = useState(null); const [copied, setCopied] = useState(false); const [balanceEth, setBalanceEth] = useState(null); const [chainOk, setChainOk] = useState(null); const [chainNote, setChainNote] = useState(null); const [lastUsed, setLastUsed] = useState(null); const nextAfterUnlock = searchParams.get("next"); const refresh = useCallback(() => { const vault = readVault(); setExists(!!vault); setAddress(vault?.address ?? null); setUnlocked(readUnlockedSession()); setLastUsed(readLastLoginMethod()); }, []); const loadBalance = useCallback(async (addr: `0x${string}`) => { const b = await fetchEthBalance(addr); const net = getActiveNetwork(); if (b.ok) { setBalanceEth(b.eth); setChainOk(true); setChainNote( `${b.networkLabel ?? net.shortLabel} · chain id ${b.chainId ?? net.chainId}`, ); } else { setBalanceEth(null); setChainOk(false); setChainNote(b.error ?? `${net.shortLabel} offline`); } }, []); useEffect(() => { setMounted(true); refresh(); const onUnlock = () => setUnlocked(readUnlockedSession()); window.addEventListener("mavi-unlock", onUnlock); window.addEventListener("mavi-network", () => { const a = readVault()?.address as `0x${string}` | undefined; if (a) void loadBalance(a); }); return () => { window.removeEventListener("mavi-unlock", onUnlock); }; }, [loadBalance, refresh]); useEffect(() => { const addr = (unlocked?.address ?? address) as `0x${string}` | null; if (!addr || !exists) return; void loadBalance(addr); const t = setInterval(() => void loadBalance(addr), 12_000); return () => clearInterval(t); }, [address, exists, loadBalance, unlocked?.address]); function handleUnlocked(u: UnlockedMavi) { setUnlocked(u); setInfo("Unlocked for this browser tab — Send and tools work until you lock."); void loadBalance(u.address); if (nextAfterUnlock?.startsWith("/portal/mavi")) { router.replace(nextAfterUnlock); } } function onLock() { lockSession(); setUnlocked(null); setInfo("Locked. Unlock once to use Send and Get test ETH in this tab."); } function onCopy() { const a = unlocked?.address ?? address; if (!a) return; void navigator.clipboard.writeText(a).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); } async function onFund() { if (!unlocked) { setError("Unlock mavi first, then get test ETH."); return; } setError(null); setInfo(null); setBusy(true); try { const { hash } = await fundFromAnvil(unlocked.address, "1"); setInfo(`Test ETH sent. Tx ${hash.slice(0, 12)}…`); addActivity({ kind: "fund", wallet: unlocked.address, hash, to: unlocked.address, amountEth: "1", note: `Test bank · ${getActiveNetwork().shortLabel}`, }); notifyActivityChanged(); await loadBalance(unlocked.address); } catch (e) { const msg = e instanceof Error ? e.message : "Fund failed"; setError( /connect|fetch|network|ECONNREFUSED/i.test(msg) ? "Local chain is not running. Open a terminal and run: bun run chains:up" : msg, ); } finally { setBusy(false); } } if (!mounted) { return ( Loading mavi… ); } if (!exists) { return (
Create mavi wallet Import recovery phrase
} /> {lastUsed === "mavi" ? (

You last signed in with mavi on this browser — create or import again to continue.

) : null} ); } return (
mavi wallet {unlocked ? "Unlocked" : "Locked"} {lastUsed === "mavi" ? ( Last used to sign in ) : null}
{unlocked ? "This tab is unlocked — balance, send, and test ETH work until you lock or close the tab." : "Locked — unlock once for this tab, then use Send without typing the password again."} {address ? ` · ${shortAddress(address)}` : ""}
{error ? ( {error} ) : null} {info ? (

{info}

) : null} { const a = (unlocked?.address ?? address) as | `0x${string}` | null; if (a) void loadBalance(a); }} />

Address

{unlocked?.address ?? address}

Balance

{balanceEth != null ? `${formatEth(balanceEth)} ETH` : chainOk === false ? "—" : "…"}

{chainNote ?? (chainOk === null ? "Checking chain…" : null)}

{unlocked ? (
Send Receive Portal sign-in
) : (
Receive (address + QR)
)}
{address ? : null} More Import another phrase only after removing this vault (or confirm replace on import). Import recovery phrase Create new (after remove) Activity

Browser add-on with its own encrypted keys:{" "} apps/mavi-extension — run{" "} bun run --cwd apps/mavi-extension build{" "} then load unpacked (Chrome, Brave, Edge, Opera, Firefox). Portal and extension vaults are separate storage.

Danger zone Removing the local vault only deletes the encrypted copy in this browser. Keep your recovery phrase safe.
); } function formatEth(eth: string): string { const n = Number(eth); if (!Number.isFinite(n)) return eth; if (n === 0) return "0"; if (n >= 1000) return n.toFixed(2); if (n >= 1) return n.toFixed(4).replace(/\.?0+$/, ""); return n.toFixed(6).replace(/\.?0+$/, ""); }