Create an app for phones import { useState, useRef, useEffect } from "react"; const DEFAULT_ASSETS = [ { id: 1, symbol: "BTC", name: "Bitcoin", amount: "2.48310000", value: "164,821.04", change: "+2.34%", positive: true, color: "#F7931A", bg: "#F7931A18" }, { id: 2, symbol: "ETH", name: "Ethereum", amount: "18.92500000", value: "58,114.75", change: "+1.12%", positive: true, color: "#627EEA", bg: "#627EEA18" }, { id: 3, symbol: "SOL", name: "Solana", amount: "312.75000000", value: "46,131.75", change: "-0.87%", positive: false, color: "#9945FF", bg: "#9945FF18" }, { id: 4, symbol: "XRP", name: "Ripple", amount: "45,200.00000000", value: "110,388.00", change: "+3.45%", positive: true, color: "#00AAE4", bg: "#00AAE418" }, { id: 5, symbol: "AVAX", name: "Avalanche", amount: "890.50000000", value: "28,416.25", change: "-1.22%", positive: false, color: "#E84142", bg: "#E8414218" }, ]; const SPARKLINES = { BTC: [40,44,42,48,46,52,50,58,54,60,57,64], ETH: [30,32,31,35,33,30,28,32,36,34,38,40], SOL: [55,58,60,57,54,50,48,52,49,46,44,42], XRP: [20,22,25,24,28,30,32,35,38,36,40,44], AVAX: [60,58,62,60,56,54,58,55,52,50,48,46], }; const fmtBal = (v) => { const n = parseFloat(String(v).replace(/,/g, "")); return isNaN(n) ? v : n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; /* ── icons ── */ const GearIcon = ({ s = 18, c = "#9B9B9B" }) => ( <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <circle cx="12" cy="12" r="3"/> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> </svg> ); const BackIcon = () => ( <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <polyline points="15 18 9 12 15 6"/> </svg> ); const PlusIcon = () => ( <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"> <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/> </svg> ); const TrashIcon = () => ( <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FF4C4C" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/> </svg> ); const CheckIcon = () => ( <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> <polyline points="20 6 9 17 4 12"/> </svg> ); const SendIcon = () => ( <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/> </svg> ); const ReceiveIcon = () => ( <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <polyline points="8 17 12 21 16 17"/><line x1="12" y1="21" x2="12" y2="3"/> </svg> ); const SwapIcon = () => ( <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/> </svg> ); const LockIcon = ({ s = 18, c = "#fff" }) => ( <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/> </svg> ); const ShieldIcon = ({ s = 34, c = "#7B61FF" }) => ( <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"> <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/> </svg> ); const DownloadIcon = ({ s = 18, c = "currentColor" }) => ( <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/> </svg> ); const CloseIcon = ({ s = 16, c = "#fff" }) => ( <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke={c} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> </svg> ); const DeleteKeyIcon = () => ( <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/><line x1="18" y1="9" x2="12" y2="15"/><line x1="12" y1="9" x2="18" y2="15"/> </svg> ); /* ── Sparkline ── */ function Sparkline({ data, color }) { const w = 80, h = 32; const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const pts = data.map((v, i) => { const x = (i / (data.length - 1)) * w; const y = h - ((v - min) / range) * (h - 4) - 2; return `${x},${y}`; }).join(" "); return ( <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`}> <polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.8"/> </svg> ); } /* ── Detail Sparkline (larger) ── */ function DetailSparkline({ data, color }) { const w = 320, h = 80; const min = Math.min(...data), max = Math.max(...data); const range = max - min || 1; const pts = data.map((v, i) => { const x = (i / (data.length - 1)) * w; const y = h - ((v - min) / range) * (h - 8) - 4; return `${x},${y}`; }); const pathD = "M " + pts.join(" L "); const areaD = pathD + ` L ${w},${h} L 0,${h} Z`; return ( <svg width="100%" height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none"> <defs> <linearGradient id="sparkGrad" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stopColor={color} stopOpacity="0.25"/> <stop offset="100%" stopColor={color} stopOpacity="0"/> </linearGradient> </defs> <path d={areaD} fill="url(#sparkGrad)"/> <path d={pathD} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> </svg> ); } const MOCK_TX = (asset) => [ { id: 1, type: "Received", date: "Jun 18, 2026", amt: `+0.12 ${asset.symbol}`, pos: true }, { id: 2, type: "Sent", date: "Jun 14, 2026", amt: `-0.05 ${asset.symbol}`, pos: false }, { id: 3, type: "Received", date: "Jun 09, 2026", amt: `+0.31 ${asset.symbol}`, pos: true }, ]; const STORAGE_KEYS = { pin: "wallet:pin", nickname: "wallet:nickname", balance: "wallet:balance", assets: "wallet:assets", }; export default function EnormousWallet() { const [loading, setLoading] = useState(true); /* ── wallet data state ── */ const [assets, setAssets] = useState(DEFAULT_ASSETS); const [totalBalance, setTotalBalance] = useState("408,271.79"); const [selected, setSelected] = useState(null); const [settingsOpen, setSettingsOpen] = useState(false); const [settingsTab, setSettingsTab] = useState("balance"); const [toast, setToast] = useState(null); const [editBalance, setEditBalance] = useState(""); const [editingId, setEditingId] = useState(null); const [editAmt, setEditAmt] = useState(""); const [newSym, setNewSym] = useState(""); const [newName, setNewName] = useState(""); const [newAmt, setNewAmt] = useState(""); const [activeTime, setActiveTime] = useState("1W"); const toastRef = useRef(null); /* ── security (PIN) state ── NOTE: this artifact environment cannot use localStorage/sessionStorage, so the PIN and nickname only persist for this session (they reset on refresh). If you move this into your own app, swap the in-memory state below for a secure on-device store (e.g. Keychain/Keystore, never plain localStorage for a PIN). */ const [pinStage, setPinStage] = useState("loading"); // "loading" | "create" | "confirm" | "locked" | "unlocked" const [storedPin, setStoredPin] = useState(null); const [pendingPin, setPendingPin] = useState(null); const [digits, setDigits] = useState(""); const [pinError, setPinError] = useState(""); const [shake, setShake] = useState(false); /* ── wallet nickname state ── */ const [nickname, setNickname] = useState(""); const [nicknameInput, setNicknameInput] = useState(""); const showToast = (msg, success = true) => { setToast({ msg, success }); clearTimeout(toastRef.current); toastRef.current = setTimeout(() => setToast(null), 2600); }; /* ── persistent storage: saves across closing/reopening the app ── */ const persist = async (key, value) => { try { await window.storage.set(key, value, false); } catch (e) { console.error("Storage error:", e); } }; useEffect(() => { (async () => { let loadedPin = null; try { const r = await window.storage.get(STORAGE_KEYS.pin, false); if (r) loadedPin = r.value; } catch (e) { /* no pin saved yet */ } try { const r = await window.storage.get(STORAGE_KEYS.nickname, false); if (r) setNickname(r.value); } catch (e) { /* no name saved yet */ } try { const r = await window.storage.get(STORAGE_KEYS.balance, false); if (r) setTotalBalance(r.value); } catch (e) { /* default balance stays */ } try { const r = await window.storage.get(STORAGE_KEYS.assets, false); if (r) setAssets(JSON.parse(r.value)); } catch (e) { /* default assets stay */ } if (loadedPin) { setStoredPin(loadedPin); setPinStage("locked"); } else { setPinStage("create"); } setLoading(false); })(); }, []); /* ── PIN entry handling ── */ const triggerShake = (msg) => { setPinError(msg); setShake(true); setTimeout(() => { setShake(false); setDigits(""); }, 420); }; const handleKey = (k) => { if (pinError) setPinError(""); if (k === "del") { setDigits((d) => d.slice(0, -1)); return; } if (digits.length >= 4) return; const next = digits + k; setDigits(next); if (next.length === 4) { setTimeout(() => submitPin(next), 120); } }; const submitPin = (val) => { if (pinStage === "create") { setPendingPin(val); setPinStage("confirm"); setDigits(""); } else if (pinStage === "confirm") { if (val === pendingPin) { setStoredPin(val); setPinStage("unlocked"); setDigits(""); showToast("Code created"); persist(STORAGE_KEYS.pin, val); } else { triggerShake("Codes don't match — try again"); setPinStage("create"); setPendingPin(null); } } else if (pinStage === "locked") { if (val === storedPin) { setPinStage("unlocked"); setDigits(""); } else { triggerShake("Incorrect code"); } } }; const lockWallet = () => { setSettingsOpen(false); setSelected(null); setDigits(""); setPinError(""); setPinStage("locked"); }; /* ── asset / balance editing ── */ const saveBalance = () => { if (editBalance.trim()) { const v = editBalance.trim(); setTotalBalance(v); setEditBalance(""); showToast("Balance updated"); persist(STORAGE_KEYS.balance, v); } }; const saveAmt = (id) => { setAssets((p) => { const next = p.map((a) => (a.id === id ? { ...a, amount: editAmt } : a)); persist(STORAGE_KEYS.assets, JSON.stringify(next)); return next; }); if (selected?.id === id) setSelected((s) => ({ ...s, amount: editAmt })); setEditingId(null); showToast("Amount saved"); }; const removeAsset = (id) => { setAssets((p) => { const next = p.filter((a) => a.id !== id); persist(STORAGE_KEYS.assets, JSON.stringify(next)); return next; }); if (selected?.id === id) setSelected(null); }; const addAsset = () => { if (!newSym.trim() || !newName.trim()) return; const colors = ["#F7931A", "#627EEA", "#9945FF", "#00AAE4", "#E84142", "#00C896", "#FF6B35"]; const c = colors[assets.length % colors.length]; setAssets((p) => { const next = [ ...p, { id: Date.now(), symbol: newSym.toUpperCase().trim(), name: newName.trim(), amount: newAmt.trim() || "0.00000000", value: "0.00", change: "0.00%", positive: true, color: c, bg: c + "18" }, ]; persist(STORAGE_KEYS.assets, JSON.stringify(next)); return next; }); setNewSym(""); setNewName(""); setNewAmt(""); showToast(`${newSym.toUpperCase()} added`); }; const saveNickname = () => { const v = nicknameInput.trim(); if (!v) return; setNickname(v); setNicknameInput(""); showToast("Wallet name saved"); persist(STORAGE_KEYS.nickname, v); }; /* ── palette ── */ const C = { bg: "#0F0F10", card: "#181818", border: "#242424", border2: "#2C2C2C", text: "#FFFFFF", sub: "#9B9B9B", faint: "#3A3A3A", accent: "#7B61FF", green: "#00D395", red: "#FF4D4D", }; const S = { root: { minHeight: "100vh", background: C.bg, display: "flex", justifyContent: "center", fontFamily: "'Inter','SF Pro Display',-apple-system,sans-serif" }, phone: { width: "100%", maxWidth: "420px", minHeight: "100vh", background: C.bg, display: "flex", flexDirection: "column", position: "relative" }, header: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "52px 20px 14px", borderBottom: `1px solid ${C.border}` }, logoWrap: { display: "flex", alignItems: "center", gap: "10px" }, logoBox: { width: "28px", height: "28px", borderRadius: "8px", background: C.accent, display: "flex", alignItems: "center", justifyContent: "center" }, logoText: { fontSize: "13px", fontWeight: "700", letterSpacing: "0.05em", color: C.text }, headerBtns: { display: "flex", gap: "8px" }, gearBtn: { background: "none", border: `1px solid ${C.border2}`, borderRadius: "8px", width: "36px", height: "36px", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }, hero: { padding: "28px 20px 20px", borderBottom: `1px solid ${C.border}` }, heroLabel: { fontSize: "11px", fontWeight: "600", letterSpacing: "0.14em", textTransform: "uppercase", color: C.sub, marginBottom: "8px" }, heroAmount: { fontSize: "38px", fontWeight: "700", color: C.text, letterSpacing: "-0.03em", lineHeight: 1, fontVariantNumeric: "tabular-nums" }, heroChange: { marginTop: "8px", fontSize: "12px", fontWeight: "600", color: C.green }, nicknameTag: { fontSize: "11px", color: C.accent, fontWeight: "700", marginTop: "10px", background: C.accent + "18", display: "inline-block", padding: "4px 10px", borderRadius: "20px" }, sectionHead: { padding: "18px 20px 10px", display: "flex", justifyContent: "space-between", alignItems: "center" }, sectionLabel: { fontSize: "11px", fontWeight: "700", letterSpacing: "0.14em", textTransform: "uppercase", color: C.sub }, list: { overflowY: "auto", flex: 1, paddingBottom: "20px" }, row: { display: "flex", alignItems: "center", gap: "12px", padding: "14px 20px", borderBottom: `1px solid ${C.border}`, cursor: "pointer", transition: "background 0.15s" }, iconBox: (bg) => ({ width: "40px", height: "40px", borderRadius: "12px", background: bg, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: "11px", fontWeight: "800" }), rowInfo: { flex: 1, minWidth: 0 }, rowName: { fontSize: "14px", fontWeight: "600", color: C.text, marginBottom: "2px" }, rowSub: { fontSize: "12px", color: C.sub, fontVariantNumeric: "tabular-nums" }, rowRight: { textAlign: "right", flexShrink: 0 }, rowValue: { fontSize: "14px", fontWeight: "600", color: C.text, fontVariantNumeric: "tabular-nums", marginBottom: "2px" }, rowChange: (pos) => ({ fontSize: "12px", fontWeight: "600", color: pos ? C.green : C.red }), detailHeader: { display: "flex", alignItems: "center", gap: "12px", padding: "52px 20px 16px", borderBottom: `1px solid ${C.border}` }, backBtn: { background: "none", border: `1px solid ${C.border2}`, borderRadius: "8px", width: "36px", height: "36px", display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", flexShrink: 0 }, detailTitle: { fontSize: "16px", fontWeight: "700", color: C.text }, detailSub: { fontSize: "12px", color: C.sub, marginTop: "2px" }, detailHero: { padding: "24px 20px 0" }, detailAmt: { fontSize: "32px", fontWeight: "700", color: C.text, letterSpacing: "-0.02em", fontVariantNumeric: "tabular-nums" }, detailVal: { fontSize: "14px", color: C.sub, marginTop: "4px", fontVariantNumeric: "tabular-nums" }, chartWrap: { padding: "16px 0 8px" }, timeRow: { display: "flex", gap: "6px", padding: "0 20px 20px" }, timeBtn: (active) => ({ padding: "5px 12px", borderRadius: "6px", border: "none", background: active ? C.accent : "transparent", color: active ? "#fff" : C.sub, fontSize: "11px", fontWeight: "700", cursor: "pointer", letterSpacing: "0.06em" }), actionRow: { display: "flex", gap: "10px", padding: "0 20px 20px" }, actionBtn: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: "6px", padding: "14px 8px", background: C.card, border: `1px solid ${C.border2}`, borderRadius: "14px", cursor: "pointer", color: "#fff", fontSize: "12px", fontWeight: "700", letterSpacing: "0.06em" }, statsGrid: { margin: "0 20px", background: C.card, borderRadius: "14px", border: `1px solid ${C.border2}`, overflow: "hidden" }, statRow: { display: "flex", justifyContent: "space-between", alignItems: "center", padding: "13px 16px", borderBottom: `1px solid ${C.border}` }, statLabel: { fontSize: "12px", color: C.sub }, statValue: { fontSize: "12px", fontWeight: "600", color: C.text, fontVariantNumeric: "tabular-nums" }, txSection: { padding: "20px 20px 0" }, txLabel: { fontSize: "11px", fontWeight: "700", letterSpacing: "0.14em", textTransform: "uppercase", color: C.sub, marginBottom: "12px" }, txRow: { display: "flex", alignItems: "center", gap: "12px", paddingBottom: "14px" }, txDot: (c) => ({ width: "36px", height: "36px", borderRadius: "10px", background: c + "18", border: `1px solid ${c}30`, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }), txInfo: { flex: 1 }, txName: { fontSize: "13px", fontWeight: "600", color: C.text }, txDate: { fontSize: "11px", color: C.sub, marginTop: "2px" }, txAmt: (pos) => ({ fontSize: "13px", fontWeight: "700", color: pos ? C.green : C.red, fontVariantNumeric: "tabular-nums", textAlign: "right" }), overlay: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.8)", backdropFilter: "blur(6px)", zIndex: 50, display: "flex", alignItems: "flex-end", justifyContent: "center" }, panel: { width