diff --git a/apps/payments/web/src/App.tsx b/apps/payments/web/src/App.tsx index 2910ee7ce..56c4f78ff 100644 --- a/apps/payments/web/src/App.tsx +++ b/apps/payments/web/src/App.tsx @@ -9,6 +9,7 @@ import { LoaderOverlay } from './layout/LoaderOverlay'; import { ActionModal } from './layout/ActionModal'; import { DepositView } from './components/DepositView'; import { WithdrawView } from './components/WithdrawView'; +import { HowItWorksModal } from './components/HowItWorksModal'; import { OverviewView } from './views/OverviewView'; import { RewardsView } from './views/RewardsView'; import { ActivityView } from './views/ActivityView'; @@ -16,10 +17,12 @@ import { SettingsView } from './views/SettingsView'; import { ChannelsStubView } from './views/ChannelsStubView'; // EmissionsView and DiemRewardsView removed — merged into RewardsView import { AuthorizedWalletProvider } from './context/AuthorizedWalletContext'; -import { AuthorizeWalletAlert } from './layout/AuthorizeWalletAlert'; import { useAuthorizedWallet } from './context/AuthorizedWalletContext'; -export type OverlayPhase = 'deposit' | 'success' | null; +export type OverlayPhase = 'success' | null; + +// Shown once to brand-new users (no balance yet); dismissal persisted here. +const HIW_SEEN_KEY = 'antseed-payments-hiw-seen'; // New 4-item portal nav + legacy sub-pages for backwards compat const VALID_TABS = new Set([ @@ -44,6 +47,15 @@ function shouldOpenDepositFromUrl(): boolean { return action === 'deposit' || tab === 'deposit' || tab === 'deposits'; } +/** + * `?welcome` (or `?welcome=1`) force-opens the "How AntSeed works" modal, + * regardless of balance or the one-time seen flag. Handy for previewing / + * deep-linking to the onboarding explainer in any browser. + */ +function shouldOpenWelcomeFromUrl(): boolean { + return new URLSearchParams(window.location.search).has('welcome'); +} + function writeTabToUrl(tab: TabId) { const url = new URL(window.location.href); url.searchParams.set('tab', tab); @@ -57,11 +69,6 @@ function clearDepositActionFromUrl() { window.history.replaceState({}, '', url.toString()); } -function truncateAddress(addr: string): string { - if (!addr || addr.length < 10) return addr; - return `${addr.slice(0, 6)}…${addr.slice(-4)}`; -} - export function App() { const [balance, setBalance] = useState(null); const [balanceLoaded, setBalanceLoaded] = useState(false); @@ -202,7 +209,7 @@ function AppShell({ refreshBalance, }: AppShellProps) { const [justDeposited, setJustDeposited] = useState(false); - const [depositPromptDismissed, setDepositPromptDismissed] = useState(false); + const [howItWorksOpen, setHowItWorksOpen] = useState(shouldOpenWelcomeFromUrl); const authorizedWallet = useAuthorizedWallet(); const isLoading = !balanceLoaded; @@ -212,9 +219,19 @@ function AppShell({ parseFloat(balance.total) === 0 && parseFloat(balance.reserved) === 0; - let overlayPhase: OverlayPhase = null; - if (justDeposited) overlayPhase = 'success'; - else if (isEmptyBuyer && !depositPromptDismissed) overlayPhase = 'deposit'; + // First-run: greet brand-new users (no balance yet) with the "How AntSeed + // works" explainer, exactly once, then hand off to the Overview checklist. + // Dismissal is persisted, so it never nags. + useEffect(() => { + if (!isEmptyBuyer) return; + if (localStorage.getItem(HIW_SEEN_KEY) === '1') return; + localStorage.setItem(HIW_SEEN_KEY, '1'); + setHowItWorksOpen(true); + }, [isEmptyBuyer]); + + // The only blocking overlay is the post-deposit success celebration. First-run + // funding is handled inline by the Overview checklist (no separate overlay). + const overlayPhase: OverlayPhase = justDeposited ? 'success' : null; const shellBlurred = isLoading || overlayPhase !== null; @@ -224,37 +241,33 @@ function AppShell({ await refreshBalance(); }, [refreshBalance, onCloseActionModal]); - const dismissSuccess = useCallback(() => setJustDeposited(false), []); - const dismissDepositPrompt = useCallback(() => setDepositPromptDismissed(true), []); + const dismissSuccess = useCallback(() => setJustDeposited(false), []); // Navigate to channels sub-page const goToChannels = useCallback(() => onSelectTab('channels'), [onSelectTab]); const goToActivity = useCallback(() => onSelectTab('activity'), [onSelectTab]); const goToRewards = useCallback(() => onSelectTab('rewards'), [onSelectTab]); - const shortAddr = buyerEvmAddress ? truncateAddress(buyerEvmAddress) : null; - const isAuthorized = authorizedWallet.operatorSet === true; + // Safety state: the user has funds on-chain but no authorized recovery wallet. + // This is the only unrecoverable-funds risk, so it's surfaced on every tab via + // the account pill (and emphasized in the Overview checklist). + const fundedTotal = balance ? parseFloat(balance.total) : 0; + const unauthorizedAtRisk = authorizedWallet.operatorSet === false && fundedTotal > 0; return ( <>
- +
-
{/* New 4-item portal nav */} {(activeTab === 'overview' || activeTab === 'dashboard') && ( @@ -263,6 +276,7 @@ function AppShell({ config={config} onOpenDeposit={onOpenDeposit} onOpenWithdraw={onOpenWithdraw} + onOpenHowItWorks={() => setHowItWorksOpen(true)} onGoToChannels={goToChannels} onGoToActivity={goToActivity} onGoToRewards={goToRewards} @@ -288,20 +302,10 @@ function AppShell({ balance={balance} config={config} buyerEvmAddress={buyerEvmAddress} - onOpenDeposit={onOpenDeposit} - onOpenWithdraw={onOpenWithdraw} />
- + + setHowItWorksOpen(false)} + onOpenDeposit={onOpenDeposit} + /> ); } diff --git a/apps/payments/web/src/components/BudgetSection.scss b/apps/payments/web/src/components/BudgetSection.scss index c3d5e9a66..5b53ad90d 100644 --- a/apps/payments/web/src/components/BudgetSection.scss +++ b/apps/payments/web/src/components/BudgetSection.scss @@ -88,40 +88,64 @@ border: 1px solid var(--danger-border); } -/* ── Cap / threshold input row ── */ -.budget-cap-row { - display: flex; - align-items: center; - gap: var(--sp-1); +/* ── Cap / threshold input group (prefix · field · inline Set) ── */ +.budget-field { + display: inline-flex; + align-items: stretch; width: 100%; - justify-content: flex-end; + max-width: 12rem; + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: var(--radius-sm); + overflow: hidden; + transition: border-color 0.12s; + + &:focus-within { border-color: var(--accent-dark); } } -.budget-cap-prefix { - font-size: 0.8125rem; +.budget-field-prefix { + display: inline-flex; + align-items: center; + padding-left: var(--sp-3); color: var(--text-muted); - flex-shrink: 0; + font-size: 0.8125rem; } -.budget-cap-input { - width: 6rem; +.budget-field-input { + flex: 1; + min-width: 0; + background: transparent; + border: none; + outline: none; + padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-1); + font-size: 0.8125rem; font-family: var(--font-mono); - text-align: right; - /* Hide number spinner arrows for cleaner look */ + color: var(--text-primary); + /* Hide number spinner arrows for a cleaner look */ -moz-appearance: textfield; &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } + &::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } + &::placeholder { color: var(--text-faint); font-family: var(--font-sans); } } -/* ── "No cap set" placeholder ── */ -.budget-unset { +.budget-field-btn { + flex-shrink: 0; + appearance: none; + background: transparent; + border: none; + border-left: 1px solid var(--input-border); + padding: 0 var(--sp-3); + font: inherit; font-size: 0.75rem; - color: var(--text-faint); - font-style: italic; + font-weight: 600; + color: var(--accent-dark); + cursor: pointer; + transition: background 0.12s, color 0.12s; + + [data-theme="dark"] & { color: var(--accent); } + &:hover:not(:disabled) { background: var(--accent-dim); } + &:disabled { opacity: 0.45; cursor: not-allowed; color: var(--text-muted); } } /* ── Loading placeholder ── */ @@ -138,9 +162,12 @@ justify-self: stretch; } +/* Stacks (message over full-width button) so it never overflows the narrow + control column when the alert appears. */ .budget-low-balance-alert { display: flex; - align-items: center; + flex-direction: column; + align-items: stretch; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: var(--amber-dim); @@ -150,14 +177,15 @@ } .budget-low-balance-msg { - flex: 1; font-size: 0.75rem; color: var(--amber); font-weight: 500; + line-height: 1.4; } .budget-topup-btn { - flex-shrink: 0; + width: 100%; + justify-content: center; font-size: 0.75rem; padding: var(--sp-1) var(--sp-3); } diff --git a/apps/payments/web/src/components/BudgetSection.tsx b/apps/payments/web/src/components/BudgetSection.tsx index bb5d74a94..24e52a967 100644 --- a/apps/payments/web/src/components/BudgetSection.tsx +++ b/apps/payments/web/src/components/BudgetSection.tsx @@ -123,15 +123,13 @@ function BudgetMeter({
- ) : ( - No cap set - )} + ) : null} {/* Cap input */} -
- $ +
+ $
)} -
- $ +
+ $ +
+ ); + } + + // Pending reminder: dismissal is session-only, so it returns on the next visit + // while setup is incomplete. (At-risk can't be dismissed at all.) + if (hidden && !atRisk) return null; + if (!nextStep) return null; + + const subText = atRisk + ? "Your deposited funds aren't recoverable until you do." + : `Step ${nextIndex} of ${total} to start routing requests.`; + + return ( +
+ + Get started + + + + {nextStep.prompt} + {subText} + + + + {nextStep.action && ( + + )} + {!atRisk && ( + + )} + +
+ ); +} diff --git a/apps/payments/web/src/components/HowItWorksModal.tsx b/apps/payments/web/src/components/HowItWorksModal.tsx new file mode 100644 index 000000000..d28b3866c --- /dev/null +++ b/apps/payments/web/src/components/HowItWorksModal.tsx @@ -0,0 +1,144 @@ +import { useCallback, type ReactNode } from 'react'; +import { ActionModal } from '../layout/ActionModal'; + +interface HowItWorksModalProps { + isOpen: boolean; + onClose: () => void; + onOpenDeposit: () => void; +} + +/** + * Educational "How AntSeed works" explainer — Fund / Route / Settle. + * Ported from the PR #502 redesign, adapted to our ActionModal + design + * tokens, with the routing copy corrected to match today's behaviour + * (no "auto-picks the best peer" / "fan-out" — that isn't built yet). + */ +export function HowItWorksModal({ isOpen, onClose, onOpenDeposit }: HowItWorksModalProps) { + const handleStart = useCallback(() => { + onClose(); + onOpenDeposit(); + }, [onClose, onOpenDeposit]); + + return ( + +
+
    + } + /> + } + /> + } + /> +
+ +
+

+ Your signer never holds funds — it authorizes spending from a balance that always belongs to you. +

+ +
+
+
+ ); +} + +interface HiwStepProps { + index: number; + eyebrow: string; + title: string; + body: string; + glyph: ReactNode; +} + +function HiwStep({ index, eyebrow, title, body, glyph }: HiwStepProps) { + return ( +
  • + +
    + {String(index).padStart(2, '0')} + {eyebrow} +
    +

    {title}

    +

    {body}

    +
  • + ); +} + +/* ── Bespoke glyphs (inline SVG, theme-aware via currentColor + tokens) ── */ + +function DepositGlyph() { + return ( + + + + + + + + + + + $ + + + + + + ); +} + +function RouteGlyph() { + return ( + + + + + + + + + + + + + ); +} + +function StreamGlyph() { + return ( + + + + + + + + + + + + ); +} diff --git a/apps/payments/web/src/layout/ActionModal.tsx b/apps/payments/web/src/layout/ActionModal.tsx index f08e68ded..3e27fab00 100644 --- a/apps/payments/web/src/layout/ActionModal.tsx +++ b/apps/payments/web/src/layout/ActionModal.tsx @@ -6,7 +6,7 @@ interface ActionModalProps { onClose: () => void; title: string; subtitle?: string; - variant?: 'default' | 'deposit'; + variant?: 'default' | 'deposit' | 'wide'; children: ReactNode; } diff --git a/apps/payments/web/src/layout/AuthorizeWalletAlert.tsx b/apps/payments/web/src/layout/AuthorizeWalletAlert.tsx deleted file mode 100644 index bc7ad092c..000000000 --- a/apps/payments/web/src/layout/AuthorizeWalletAlert.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useAuthorizedWallet } from '../context/AuthorizedWalletContext'; - -export function AuthorizeWalletAlert() { - const { operatorSet, requireAuthorization } = useAuthorizedWallet(); - - if (operatorSet !== false) return null; - - return ( -
    - -
    -
    Your funds are not recoverable yet
    -
    - Authorize an external wallet so you can withdraw USDC, claim ANTS, and close - channels. Without one, losing this node means losing your funds. -
    -
    -
    - -
    -
    - ); -} diff --git a/apps/payments/web/src/layout/EmptyStateOverlay.tsx b/apps/payments/web/src/layout/EmptyStateOverlay.tsx index 8a5630974..50f6bb265 100644 --- a/apps/payments/web/src/layout/EmptyStateOverlay.tsx +++ b/apps/payments/web/src/layout/EmptyStateOverlay.tsx @@ -1,42 +1,9 @@ -import { useState } from 'react'; import { useBodyScrollLock } from '../hooks/useBodyScrollLock'; -import type { BalanceData, PaymentConfig } from '../types'; -import { DepositView } from '../components/DepositView'; import type { OverlayPhase } from '../App'; interface EmptyStateOverlayProps { phase: OverlayPhase; - config: PaymentConfig | null; - balance: BalanceData | null; - buyerAddress: string | null; - onDeposited: () => void; onContinue: () => void; - onDismissDeposit?: () => void; -} - -function CopyIcon() { - return ( - - ); -} - -function CheckIcon() { - return ( - - ); -} - -function CloseIcon() { - return ( - - ); } function BigCheckIcon() { @@ -48,89 +15,36 @@ function BigCheckIcon() { ); } -export function EmptyStateOverlay({ - phase, - config, - balance, - buyerAddress, - onDeposited, - onContinue, - onDismissDeposit, -}: EmptyStateOverlayProps) { - const [copied, setCopied] = useState(false); - +/** + * Post-deposit success celebration. First-run funding is handled inline by the + * Overview "Get started" checklist, so this overlay only renders the 'success' + * phase after a deposit completes. + */ +export function EmptyStateOverlay({ phase, onContinue }: EmptyStateOverlayProps) { const isVisible = phase !== null; useBodyScrollLock(isVisible); if (!isVisible) return null; - async function handleCopy() { - if (!buyerAddress) return; - try { - await navigator.clipboard.writeText(buyerAddress); - setCopied(true); - window.setTimeout(() => setCopied(false), 1400); - } catch { - // clipboard blocked — ignore - } - } - return ( -
    +
    - {phase === 'deposit' ? ( - <> - {onDismissDeposit && ( - - )} -
    -
    Welcome to AntSeed
    -

    Fund your AntSeed account

    -

    - Deposit USDC to start routing requests across the network. Your AntSeed - signer authorizes spending from the account — it never holds funds itself. -

    -
    - -
    -
    Step 1 · Deposit USDC
    - -
    - - ) : ( -
    -
    - -
    -

    You're all set

    -

    - Your deposit is in. AntSeed will now route requests across the network — - you only pay for what you use. -

    -
    - -
    +
    +
    + +
    +

    You're all set

    +

    + Your deposit is in. AntSeed will now route requests across the network — + you only pay for what you use. +

    +
    +
    - )} +
    ); diff --git a/apps/payments/web/src/layout/Sidebar.tsx b/apps/payments/web/src/layout/Sidebar.tsx index a7d64107e..cc0a05de2 100644 --- a/apps/payments/web/src/layout/Sidebar.tsx +++ b/apps/payments/web/src/layout/Sidebar.tsx @@ -17,13 +17,6 @@ export type TabId = interface SidebarProps { activeTab: TabId; onSelect: (tab: TabId) => void; - isDark: boolean; - onToggleTheme: () => void; - /** Short-form wallet address, e.g. "0x3f9a…c2a1" */ - walletAddress?: string | null; - /** Whether the wallet is authorized */ - walletAuthorized?: boolean; - onOpenWallet?: () => void; } interface NavItem { @@ -35,17 +28,27 @@ interface NavItem { /* ── SVG icons ── */ function OverviewIcon() { + // Grid / dashboard — "account at a glance" return ( -
    - {walletAddress ? ( - <> - -
    -
    - - ) : ( -
    - No wallet - -
    - )} -
    ); } diff --git a/apps/payments/web/src/layout/TopBar.tsx b/apps/payments/web/src/layout/TopBar.tsx index a6c059a27..c9823f3bd 100644 --- a/apps/payments/web/src/layout/TopBar.tsx +++ b/apps/payments/web/src/layout/TopBar.tsx @@ -4,8 +4,12 @@ import type { BalanceData } from '../types'; interface TopBarProps { activeTab: TabId; balance: BalanceData | null; + buyerEvmAddress: string | null; + /** Funds on-chain but no authorized recovery wallet — surfaces a warning on the pill. */ + atRisk: boolean; + isDark: boolean; + onToggleTheme: () => void; onOpenWallet: () => void; - onOpenDeposit: () => void; } const TAB_TITLES: Record = { @@ -36,6 +40,10 @@ function formatUsd(n: number): string { return n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } +function truncateAddr(addr: string): string { + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + function SignerIcon() { return (
    {TAB_SUBTITLES[activeTab]}
    -
    - + - -
    + + +
    ); diff --git a/apps/payments/web/src/layout/WalletDrawer.tsx b/apps/payments/web/src/layout/WalletDrawer.tsx index d3767d75d..ef74e0e09 100644 --- a/apps/payments/web/src/layout/WalletDrawer.tsx +++ b/apps/payments/web/src/layout/WalletDrawer.tsx @@ -4,7 +4,6 @@ import { ConnectButton } from '@rainbow-me/rainbowkit'; import type { BalanceData, PaymentConfig } from '../types'; import { useSetOperator, useTransferOperator } from '../hooks/useSetOperator'; import { useAuthorizedWallet } from '../context/AuthorizedWalletContext'; -import { Button } from '../components/Button'; import { InfoHint } from '../components/InfoHint'; import { useBodyScrollLock } from '../hooks/useBodyScrollLock'; @@ -14,8 +13,6 @@ interface WalletDrawerProps { balance: BalanceData | null; config: PaymentConfig | null; buyerEvmAddress: string | null; - onOpenDeposit: () => void; - onOpenWithdraw: () => void; } const ZERO_ADDR = '0x0000000000000000000000000000000000000000'; @@ -73,8 +70,6 @@ export function WalletDrawer({ balance, config, buyerEvmAddress, - onOpenDeposit, - onOpenWithdraw, }: WalletDrawerProps) { const { address: connectedAddress, isConnected, connector } = useAccount(); const { disconnect } = useDisconnect(); @@ -121,14 +116,6 @@ export function WalletDrawer({ const reserved = balance ? parseFloat(balance.reserved) : 0; const total = balance ? parseFloat(balance.total) : 0; - function handleDepositMore() { - onOpenDeposit(); - } - - function handleWithdraw() { - onOpenWithdraw(); - } - return ( <>
    -
    - - -
    {/* ── Your wallet ───────────────────────────────────── */} diff --git a/apps/payments/web/src/styles/global.scss b/apps/payments/web/src/styles/global.scss index 7bf92de0d..1eeae522a 100644 --- a/apps/payments/web/src/styles/global.scss +++ b/apps/payments/web/src/styles/global.scss @@ -23,6 +23,11 @@ /* Accent */ --accent: #1fd87a; --accent-text: #06281a; + /* Label colour for text that sits ON the solid accent fill (filled + buttons). The accent fill is bright green in BOTH themes, so this + stays dark in both — unlike --accent-text, which dark mode flips to + green for accent-coloured text on neutral backgrounds. */ + --on-accent: #06281a; --accent-dark: #16a85e; --accent-dim: rgba(31, 216, 122, 0.08); --accent-border: rgba(31, 216, 122, 0.35); @@ -154,7 +159,7 @@ html, body { color: var(--text-primary); min-height: 100vh; -webkit-font-smoothing: antialiased; - font-size: 0.875rem; /* 14px */ + font-size: 1rem; /* 16px */ line-height: 1.5; } @@ -240,57 +245,9 @@ $sidebar-width: 184px; } /* Wallet footer at the bottom of the sidebar */ -.portal-wallet-footer { - margin-top: auto; - padding: 0.875rem var(--sp-4); - font-size: 0.6875rem; - color: var(--sub); - border-top: 1px solid var(--border); - - .portal-wallet-addr { - color: var(--ink); - font-weight: 600; - font-family: var(--font-mono); - font-size: 0.75rem; - cursor: pointer; - - &:hover { color: var(--accent-dark); } - } - .portal-wallet-status { - margin-top: 0.25rem; - display: flex; - align-items: center; - gap: 0.375rem; - } - .portal-wallet-ok { - color: var(--accent-dark); - font-weight: 600; - } - .portal-wallet-dot { - width: 0.375rem; - height: 0.375rem; - border-radius: 50%; - background: var(--accent); - flex-shrink: 0; - } - .portal-wallet-theme-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.75rem; - height: 1.75rem; - border-radius: var(--radius-sm); - border: 1px solid var(--border-2); - background: transparent; - color: var(--sub); - cursor: pointer; - margin-left: auto; - font-family: inherit; - font-size: 0.75rem; - transition: color 0.12s, background 0.12s; - &:hover { color: var(--ink); background: var(--card-hover); } - } -} +/* The sidebar wallet/identity footer and its theme toggle were retired — the + account pill (top-right) carries identity, and the theme toggle now lives in + the top bar (.dash-topbar-theme-toggle). */ /* ── Main content area ── */ .portal-main { @@ -305,20 +262,6 @@ $sidebar-width: 184px; width: 100%; } -/* ── Page header (h1 + subtitle pattern) ── */ -.page-h1 { - font-size: 1.25rem; - font-weight: 600; - color: var(--ink); - letter-spacing: -0.01em; -} - -.page-subtitle { - font-size: 0.75rem; - color: var(--muted); - margin-top: 0.125rem; -} - .page-rule { height: 1px; background: var(--border); @@ -363,7 +306,7 @@ $sidebar-width: 184px; &.primary { background: var(--accent); - color: var(--accent-text); + color: var(--on-accent); border: none; &:hover:not(:disabled) { filter: brightness(1.06); } &:active:not(:disabled) { filter: brightness(0.96); } @@ -394,7 +337,7 @@ $sidebar-width: 184px; font-family: inherit; width: 100%; background: var(--accent); - color: var(--accent-text); + color: var(--on-accent); border: none; transition: filter 0.12s; &:hover:not(:disabled) { filter: brightness(1.06); } @@ -949,74 +892,100 @@ $dash-sidebar-width: $sidebar-width; gap: var(--sp-4); &-titles { display: flex; flex-direction: column; gap: 0.125rem; min-width: 0; } - &-title { font-size: 1.125rem; font-weight: 600; color: var(--text-primary); } + &-title { font-size: 1.5rem; font-weight: 600; color: var(--text-primary); letter-spacing: -0.01em; } &-subtitle { font-size: 0.75rem; color: var(--text-muted); } - &-right { display: flex; align-items: center; gap: 0.75rem; } - &-balance-group { + &-right { display: flex; align-items: center; gap: 0.625rem; } + + /* ── Theme toggle (light/dark) ── */ + &-theme-toggle { display: inline-flex; - align-items: stretch; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 999px; background: var(--card-bg); border: 1px solid var(--border); - border-radius: var(--radius-xl); - overflow: hidden; + color: var(--text-secondary); + cursor: pointer; + transition: border-color 0.14s, background 0.14s, color 0.14s; + &:hover { border-color: var(--border-2); background: var(--card-hover); color: var(--text-primary); } } - &-wallet { + + /* ── Account pill (balance + signer address → opens account settings) ── */ + &-account { display: inline-flex; align-items: center; gap: 0.625rem; - padding: 0.5rem 0.875rem 0.5rem 0.75rem; - background: transparent; - border: none; - color: var(--sub); + padding: 0.375rem 0.625rem 0.375rem 0.5rem; + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + color: var(--ink); cursor: pointer; font: inherit; - transition: color 0.14s; - &:hover { color: var(--ink); } + transition: border-color 0.14s, background 0.14s; + &:hover { border-color: var(--border-2); background: var(--card-hover); } } - &-wallet-icon { + &-account-avatar { display: inline-flex; align-items: center; justify-content: center; - width: 1.625rem; - height: 1.625rem; + width: 1.75rem; + height: 1.75rem; border-radius: 999px; background: var(--accent-dim); color: var(--accent-text); + flex-shrink: 0; } - &-wallet-text { + &-account-text { display: flex; flex-direction: column; align-items: flex-start; - gap: 0.125rem; + gap: 0.0625rem; + line-height: 1.15; min-width: 0; } - &-wallet-label { - font-size: 0.5625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); - line-height: 1; - } - &-wallet-value { - font-size: 0.75rem; - font-family: var(--font-mono); + &-account-balance { + font-size: 0.8125rem; + font-weight: 700; font-variant-numeric: tabular-nums; - line-height: 1.2; color: var(--ink); } - &-deposit-btn { - border: none; - border-left: 1px solid var(--border); - background: var(--accent); - color: var(--accent-text); - padding: 0 0.875rem; - font: inherit; - font-size: 0.75rem; - font-weight: 700; - cursor: pointer; - transition: filter 0.14s; - &:hover { filter: brightness(0.96); } + &-account-addr { + font-size: 0.6875rem; + font-family: var(--font-mono); + color: var(--muted); + } + &-account-chevron { + display: inline-flex; + align-items: center; + color: var(--muted); + margin-left: 0.125rem; + } + + /* Funds-at-risk: amber-tinted pill + warning avatar + badge, on every tab. */ + &-account--risk { + border-color: var(--amber-border); + background: var(--amber-dim); + &:hover { border-color: var(--amber); background: var(--amber-dim); } + + .dash-topbar-account-avatar { + background: var(--amber-dim); + color: var(--amber); + } + .dash-topbar-account-addr { color: var(--amber); font-family: var(--font-sans); font-weight: 600; } + } + &-account-avatar { position: relative; } + &-account-badge { + position: absolute; + top: -0.0625rem; + right: -0.0625rem; + width: 0.5rem; + height: 0.5rem; + border-radius: 999px; + background: var(--amber); + border: 1.5px solid var(--cream); } } @@ -1487,54 +1456,8 @@ $dash-sidebar-width: $sidebar-width; } /* ── authorize wallet alert ── */ -.authorize-wallet-alert { - display: flex; - align-items: flex-start; - gap: 0.875rem; - margin: var(--sp-4) 2rem 0; - padding: 0.875rem var(--sp-4); - border-radius: var(--radius-lg); - background: var(--amber-dim); - border: 1px solid var(--amber-border); - - &-icon { - flex-shrink: 0; - width: 1.5rem; - height: 1.5rem; - border-radius: 50%; - background: var(--amber); - color: #fff; - font-weight: 700; - font-size: 0.8125rem; - display: inline-flex; - align-items: center; - justify-content: center; - } - &-body { flex: 1; min-width: 0; } - &-title { font-size: 0.8125rem; font-weight: 600; color: var(--amber); margin-bottom: 0.125rem; } - &-desc { font-size: 0.75rem; line-height: 1.5; color: var(--text-secondary); } - &-actions { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.375rem; - flex-shrink: 0; - justify-content: center; - } - &-btn { font-size: 0.75rem; padding: 0.375rem 0.875rem; } - &-dismiss { - background: none; - border: none; - padding: 0; - color: var(--text-muted); - font-size: 0.6875rem; - cursor: pointer; - text-decoration: underline; - text-underline-offset: 0.125rem; - font-family: inherit; - &:hover { color: var(--text-primary); } - } -} +/* The standalone authorize banner was retired — its safety warning now lives on + the account pill (all tabs) and the emphasized Authorize step in the checklist. */ /* ── authorize wallet modal ── */ .authorize-wallet-modal { display: flex; flex-direction: column; gap: 1.125rem; } @@ -2020,6 +1943,7 @@ $dash-sidebar-width: $sidebar-width; animation: empty-state-rise 0.3s cubic-bezier(0.22, 1, 0.36, 1) both; &--deposit { width: min(35rem, 100%); } + &--wide { width: min(44rem, 100%); } .deposit, .withdraw { width: 100%; min-width: 0; } .deposit > .card, .withdraw > .card { @@ -2131,3 +2055,270 @@ $dash-sidebar-width: $sidebar-width; border-left: 0.125rem solid var(--accent); border-radius: 0.25rem; } + +/* ────────────────────────────────────────────────────────────────── + * How AntSeed works — educational modal (`HowItWorksModal`) + * Ported from PR #502. Steps: Fund / Route / Settle with bespoke glyphs. + * ────────────────────────────────────────────────────────────────── */ + +.hiw { + display: flex; + flex-direction: column; + gap: 20px; + padding-top: 2px; +} + +/* Horizontal 3-up step flow: short and wide, close to the deposit modal's footprint. */ +.hiw-steps { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +.hiw-step { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + opacity: 0; + transform: translateY(8px); + animation: hiw-step-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; + animation-delay: var(--hiw-delay, 0ms); + + /* Subtle divider between columns (not before the first). */ + & + & { + padding-left: 20px; + border-left: 1px solid var(--divider); + } +} + +.hiw-step-glyph { + display: inline-flex; + width: 44px; + height: 44px; + color: var(--text-secondary); + + svg { width: 44px; height: 44px; } +} + +.hiw-step-eyebrow { + display: inline-flex; + align-items: center; + gap: 7px; + margin-top: 2px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--accent-text); +} + +.hiw-step-num { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 19px; + height: 19px; + padding: 0 5px; + border-radius: 6px; + background: var(--accent-dim); + border: 1px solid var(--accent-border); + color: var(--accent-text); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.02em; + font-variant-numeric: tabular-nums; +} + +.hiw-step-title { + margin: 0; + font-size: 15px; + font-weight: 600; + letter-spacing: -0.012em; + color: var(--text-primary); + line-height: 1.25; +} + +.hiw-step-text { + margin: 0; + font-size: 12.5px; + line-height: 1.5; + color: var(--text-secondary); +} + +.hiw-foot { + display: flex; + flex-direction: column; + gap: 14px; + padding-top: 18px; + border-top: 1px solid var(--divider); +} + +.hiw-note { + margin: 0 auto; + font-size: 12px; + line-height: 1.55; + color: var(--text-muted); + text-align: center; + max-width: 56ch; +} + +.hiw-cta { align-self: stretch; } + +@keyframes hiw-step-in { + to { opacity: 1; transform: translateY(0); } +} + +@media (max-width: 600px) { + .hiw-steps { + grid-template-columns: 1fr; + gap: 14px; + } + .hiw-step { + & + & { + padding-left: 0; + padding-top: 14px; + border-left: none; + border-top: 1px solid var(--divider); + } + } +} + +/* ────────────────────────────────────────────────────────────────── + * Getting Started — minimal first-run banner (`GettingStarted`) + * A single slim row at the top of Overview: eyebrow + progress dots, + * the next-step prompt, its action, a "How it works" link, and dismiss. + * Goes amber and non-dismissable when funds are at risk (unauthorized). + * ────────────────────────────────────────────────────────────────── */ + +.gs-banner { + display: flex; + align-items: center; + gap: 14px; + margin: 6px 0 18px; + padding: 10px 14px; + background: var(--box-bg); + border: 1px solid var(--card-border); + border-radius: 12px; +} + +.gs-banner-lead { + display: inline-flex; + align-items: center; + gap: 9px; + flex-shrink: 0; +} + +.gs-banner-eyebrow { + font-size: 9.5px; + font-weight: 700; + letter-spacing: 0.13em; + text-transform: uppercase; + color: var(--text-muted); + line-height: 1; +} + +.gs-banner-dots { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.gs-bdot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--card-border); + + &--done { background: var(--accent); } +} + +.gs-banner-text { + flex: 1; + min-width: 0; + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 2px 8px; + line-height: 1.3; +} + +.gs-banner-title { + font-size: 13px; + font-weight: 600; + letter-spacing: -0.006em; + color: var(--text-primary); +} + +.gs-banner-sub { + font-size: 12px; + color: var(--text-muted); +} + +.gs-banner-actions { + display: inline-flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.gs-banner-btn { + display: inline-flex; + align-items: center; + height: 28px; + padding: 0 14px; + border-radius: 999px; + background: var(--accent); + border: 1px solid var(--accent); + color: var(--on-accent); + font: inherit; + font-size: 12px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + transition: filter 0.14s ease; + + &:hover { filter: brightness(0.96); } +} + +.gs-banner-x { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 999px; + background: transparent; + border: none; + color: var(--text-faint); + cursor: pointer; + transition: color 0.14s ease, background 0.14s ease; + + &:hover { color: var(--text-primary); background: var(--card-hover); } +} + +.gs-banner-check { + display: inline-flex; + align-items: center; + color: var(--accent); + flex-shrink: 0; +} + +/* Risk variant: funded but unauthorized — amber, non-dismissable. */ +.gs-banner--risk { + background: var(--amber-dim); + border-color: var(--amber-border); + + .gs-banner-eyebrow { color: var(--amber); } + .gs-banner-title { color: var(--amber); } + .gs-banner-sub { color: var(--amber); } + .gs-bdot--done { background: var(--amber); } +} + +@media (max-width: 40rem) { + .gs-banner { flex-wrap: wrap; } + .gs-banner-text { order: 3; flex-basis: 100%; } +} diff --git a/apps/payments/web/src/views/ActivityView.tsx b/apps/payments/web/src/views/ActivityView.tsx index 970d705c5..500fc93a1 100644 --- a/apps/payments/web/src/views/ActivityView.tsx +++ b/apps/payments/web/src/views/ActivityView.tsx @@ -116,9 +116,7 @@ export function ActivityView({ config: _config }: ActivityViewProps) { return (
    - {/* Page header */} -
    Activity
    -
    Settlements and channel closes
    + {/* Page title/subtitle live in the sticky TopBar (layout/TopBar.tsx). */} {/* Stale data notice */} void; onOpenWithdraw: () => void; + onOpenHowItWorks: () => void; onGoToChannels: () => void; onGoToActivity: () => void; onGoToRewards: () => void; @@ -38,6 +40,7 @@ export function OverviewView({ config: _config, onOpenDeposit, onOpenWithdraw, + onOpenHowItWorks, onGoToChannels, onGoToActivity, }: OverviewViewProps) { @@ -72,9 +75,7 @@ export function OverviewView({ // ── Derived values ──────────────────────────────────────────────────────── const available = balance?.available ?? null; const reserved = balance?.reserved ?? null; - const total = balance?.total ?? null; - const totalUsd = total ? parseFloat(total) : null; const availableUsd = available ? parseFloat(available) : null; const reservedUsd = reserved ? parseFloat(reserved) : null; @@ -94,9 +95,13 @@ export function OverviewView({ return (
    - {/* Page header */} -
    Overview
    -
    Your AntSeed account at a glance
    + {/* Page title/subtitle live in the sticky TopBar (layout/TopBar.tsx). */} + + {/* Onboarding checklist — self-hides once dismissed / all steps done */} + {/* Stale data notice */}
    Available balance
    - {totalUsd !== null ? ( + {availableUsd !== null ? ( <> - ${totalUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{' '} + ${availableUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{' '} USDC ) : ( @@ -132,10 +137,9 @@ export function OverviewView({ )}
    - {availableUsd !== null && reservedUsd !== null ? ( + {reservedUsd !== null && reservedUsd > 0 ? ( <> - ${formatUsd(available)} available · ${formatUsd(reserved)} in{' '} - {activeChannels} active channel{activeChannels !== 1 ? 's' : ''}{' '} + ${formatUsd(reserved)} reserved in {activeChannels} active channel{activeChannels !== 1 ? 's' : ''}{' '} ·{' '} ) : null} diff --git a/apps/payments/web/src/views/RewardsView.scss b/apps/payments/web/src/views/RewardsView.scss index b36a037d2..52302d57b 100644 --- a/apps/payments/web/src/views/RewardsView.scss +++ b/apps/payments/web/src/views/RewardsView.scss @@ -43,7 +43,7 @@ width: auto; flex-shrink: 0; background: var(--accent); - color: var(--accent-text); + color: var(--on-accent); border: none; border-radius: var(--radius-md); padding: 0.625rem 1.125rem; @@ -229,8 +229,12 @@ .rewards-diem-connect { display: flex; flex-direction: column; + align-items: flex-start; gap: var(--sp-3); margin-bottom: var(--sp-4); + + /* .btn-primary is full-width by default (form style); size to content here. */ + .btn-primary { width: auto; } } .rewards-diem-connect-desc { @@ -322,7 +326,7 @@ &.primary { background: var(--accent); - color: var(--accent-text); + color: var(--on-accent); border: none; border-radius: var(--radius-md); padding: 0.625rem 1.125rem; diff --git a/apps/payments/web/src/views/RewardsView.tsx b/apps/payments/web/src/views/RewardsView.tsx index 6e33b09dc..bfafce300 100644 --- a/apps/payments/web/src/views/RewardsView.tsx +++ b/apps/payments/web/src/views/RewardsView.tsx @@ -632,8 +632,7 @@ export function RewardsView({ config }: RewardsViewProps) { if (!isLoading && emissionsError && !emissionsInfo) { return (
    -
    Rewards
    -
    $ANTS earned from network usage and DIEM staking
    + {/* Page title/subtitle live in the sticky TopBar (layout/TopBar.tsx). */}
    Rewards not available
    @@ -675,8 +674,7 @@ export function RewardsView({ config }: RewardsViewProps) { return (
    -
    Rewards
    -
    $ANTS earned from network usage and DIEM staking
    + {/* Page title/subtitle live in the sticky TopBar (layout/TopBar.tsx). */} {/* ── Hero: claimable total + Claim all button ── */}
    @@ -816,7 +814,18 @@ export function RewardsView({ config }: RewardsViewProps) {

    Connect the wallet you used on the DIEM staking portal to view and claim $ANTS.

    - + + {({ openConnectModal, mounted }) => ( + + )} +
    )} diff --git a/apps/payments/web/src/views/SettingsView.scss b/apps/payments/web/src/views/SettingsView.scss index 6c2abc28a..762d6fabd 100644 --- a/apps/payments/web/src/views/SettingsView.scss +++ b/apps/payments/web/src/views/SettingsView.scss @@ -6,16 +6,33 @@ .settings-view { display: flex; flex-direction: column; - max-width: 42rem; + max-width: 66rem; } -/* ── Page header ── */ -.settings-header { - margin-bottom: var(--sp-7); +/* Budget | Wallet (equal height) on top, Network full-width beneath. */ +.settings-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--sp-5); + align-items: stretch; +} + +.settings-cell { + display: flex; + flex-direction: column; + min-width: 0; } -/* ── Budget slot (filled by BudgetSection component) ── */ -/* .settings-budget-slot is no longer rendered; BudgetSection renders directly. */ +/* Card fills its cell so the two top cards align to the same height. */ +.settings-cell > .settings-card { flex: 1; } + +/* Network spans both columns underneath. */ +.settings-cell--full { grid-column: 1 / -1; } + +@media (max-width: 60rem) { + .settings-grid { grid-template-columns: 1fr; } + .settings-cell--full { grid-column: auto; } +} /* ── Section label ── */ .settings-section-label { @@ -29,7 +46,8 @@ padding-bottom: var(--sp-1); } -.settings-section-label:first-of-type { +/* First label in each cell hugs the top. */ +.settings-cell > .settings-section-label:first-child { margin-top: 0; } @@ -44,8 +62,10 @@ /* ── Individual setting row (two-column: description + control) ── */ .set-item { display: grid; - grid-template-columns: minmax(0, 1.4fr) minmax(13.75rem, 18.75rem); - gap: 0.875rem; + /* Give the description text the lion's share; control sizes to its content + (capped) so copy isn't squeezed into a narrow ragged column. */ + grid-template-columns: minmax(0, 1fr) minmax(7rem, 12rem); + gap: var(--sp-4); align-items: center; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--divider); @@ -55,6 +75,37 @@ } } +/* Two settings rows side by side (e.g. Chain | Block explorer) with a divider + between them, and one beneath the pair. */ +.set-row-2up { + display: grid; + grid-template-columns: 1fr 1fr; + border-bottom: 1px solid var(--divider); + + > .set-item { border-bottom: none; } + > .set-item:first-child { border-right: 1px solid var(--divider); } + + @media (max-width: 60rem) { + grid-template-columns: 1fr; + > .set-item:first-child { + border-right: none; + border-bottom: 1px solid var(--divider); + } + } +} + +/* Full-width stacked row: description over a wide, full-width control + (e.g. the RPC endpoint input, which needs room for a long URL). */ +.set-item-stack { + display: flex; + flex-direction: column; + gap: var(--sp-3); + padding: var(--sp-4) var(--sp-5); + + .set-input-row { width: 100%; } + .set-input { flex: 1; } +} + .set-copy { display: flex; flex-direction: column; @@ -91,6 +142,22 @@ word-break: break-all; } +/* Connected-wallet control: address + provider chip + Disconnect, wraps tidily. */ +.set-wallet-ctrl { + flex-wrap: wrap; + justify-content: flex-end; +} + +.set-wallet-provider { + font-size: 0.6875rem; + color: var(--text-muted); + padding: 0.1875rem 0.5rem; + border-radius: 999px; + background: var(--card-hover); + border: 1px solid var(--card-border); + white-space: nowrap; +} + /* ── Status pills ── */ .set-pill { display: inline-flex; @@ -153,6 +220,13 @@ } } +/* Ghost button rendered as an (block explorer link). */ +.set-btn-link { + display: inline-flex; + align-items: center; + text-decoration: none; +} + /* ── RPC input ── */ .set-input { width: 100%; diff --git a/apps/payments/web/src/views/SettingsView.tsx b/apps/payments/web/src/views/SettingsView.tsx index 36e7d28b2..a4cbfecf8 100644 --- a/apps/payments/web/src/views/SettingsView.tsx +++ b/apps/payments/web/src/views/SettingsView.tsx @@ -1,20 +1,22 @@ /** - * Settings view — Budget · Wallet · Network · Appearance + * Settings view — Budget · Wallet · Network * - * Sections: - * Budget — monthly spend cap meter, low-balance nudge, credit-limit context - * Wallet — authorized wallet + status, transfer-authorization - * Network — chain + health pill + RPC override - * Appearance — dark theme toggle + * Two-column masonry layout: + * Left — Budget (monthly cap meter, pause-at-cap, low-balance nudge, credit limit) + * Right — Wallet (connected wallet + disconnect, authorized wallet, transfer) + * Network (chain + health, RPC override, block explorer) + * + * Theme/appearance lives in the top bar now, so it's no longer a Settings section. * * Wires to: - * - BudgetSection for budget/spend-control (issue 9) - * - AuthorizedWalletContext for wallet/operator state and authorize flow - * - AuthorizeWalletModal (via requireAuthorization) + * - BudgetSection for budget/spend-control + * - AuthorizedWalletContext for operator state and the authorize flow + * - wagmi useAccount/useDisconnect + RainbowKit for the connected wallet * - useTransferOperator for the transfer-auth flow - * - localStorage 'antseed-payments-theme' + data-theme on */ -import { useCallback, useEffect, useId, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { useAccount, useDisconnect } from 'wagmi'; +import { ConnectButton } from '@rainbow-me/rainbowkit'; import { useAuthorizedWallet } from '../context/AuthorizedWalletContext'; import { useTransferOperator } from '../hooks/useSetOperator'; import { ActionModal } from '../layout/ActionModal'; @@ -22,54 +24,11 @@ import { BudgetSection } from '../components/BudgetSection'; import type { PaymentConfig } from '../types'; import './SettingsView.scss'; -const THEME_KEY = 'antseed-payments-theme'; - function truncateAddr(addr: string | null): string { if (!addr || addr.length < 10) return addr ?? '—'; return `${addr.slice(0, 6)}…${addr.slice(-4)}`; } -/** Read the current persisted theme choice. */ -function readTheme(): boolean { - const saved = localStorage.getItem(THEME_KEY); - if (saved) return saved === 'dark'; - return window.matchMedia('(prefers-color-scheme: dark)').matches; -} - -/** Persist + apply theme to the document root. */ -function applyTheme(dark: boolean) { - document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light'); - localStorage.setItem(THEME_KEY, dark ? 'dark' : 'light'); -} - -// ── Sub-component: Toggle switch ────────────────────────────────────────────── - -interface ToggleProps { - checked: boolean; - onChange: (v: boolean) => void; - id: string; - label: string; -} - -function Toggle({ checked, onChange, id, label }: ToggleProps) { - return ( - - ); -} - // ── Sub-component: Transfer-authorization modal ─────────────────────────────── interface TransferModalProps { @@ -177,34 +136,18 @@ interface SettingsViewProps { export function SettingsView({ config, onOpenDeposit }: SettingsViewProps) { const { operator, operatorSet, requireAuthorization, refetch } = useAuthorizedWallet(); - const [isDark, setIsDark] = useState(readTheme); + const { address: connectedAddress, isConnected, connector } = useAccount(); + const { disconnect } = useDisconnect(); + const [transferOpen, setTransferOpen] = useState(false); const [rpcValue, setRpcValue] = useState(() => config?.rpcUrl ?? ''); const [rpcSaved, setRpcSaved] = useState(false); - const toggleId = useId(); - - // Keep local toggle in sync with external theme changes (e.g. sidebar toggle) - useEffect(() => { - const onStorage = (e: StorageEvent) => { - if (e.key === THEME_KEY && e.newValue) { - setIsDark(e.newValue === 'dark'); - } - }; - window.addEventListener('storage', onStorage); - return () => window.removeEventListener('storage', onStorage); - }, []); - // Keep rpcValue in sync if config loads after mount useEffect(() => { if (config?.rpcUrl) setRpcValue(config.rpcUrl); }, [config?.rpcUrl]); - const handleThemeToggle = useCallback((dark: boolean) => { - setIsDark(dark); - applyTheme(dark); - }, []); - const handleAuthorize = useCallback(() => { requireAuthorization(); }, [requireAuthorization]); @@ -216,167 +159,216 @@ export function SettingsView({ config, onOpenDeposit }: SettingsViewProps) { const handleRpcSave = useCallback(() => { // RPC override is stored locally — the server picks it up on next reload. - // For now we persist it in localStorage so the note is surfaced to the user. localStorage.setItem('antseed-rpc-override', rpcValue.trim()); setRpcSaved(true); setTimeout(() => setRpcSaved(false), 2500); }, [rpcValue]); - const chainLabel = config - ? `${config.chainId}` // e.g. "base" or "base-local" - : '—'; - + const chainLabel = config ? `${config.chainId}` : '—'; const operatorDisplay = operator ? truncateAddr(operator) : null; const walletAuthorized = operatorSet === true; - // Determine chain health. We don't have a live health endpoint here, - // so we show "online" when we have config (server responded) and "unknown" otherwise. + // Chain health: "online" when the server responded with config, else "unknown". const chainHealthy = config !== null; - return ( -
    -
    -
    Settings
    -
    Wallet, network, and appearance
    + // Block explorer — Base mainnet only (no explorer for local/anvil chains). + const explorerUrl = + config?.evmAddress && config.chainId === 'base' + ? `https://basescan.org/address/${config.evmAddress}` + : null; + + const chainItem = ( +
    +
    +

    Chain

    +

    Network the AntSeed protocol contracts live on.

    +
    + + {chainLabel} + + + +
    +
    + ); - {/* ── Budget — monthly spend cap, low-balance nudge, credit-limit context ── */} - - - {/* ── Wallet ─────────────────────────────────────────────────── */} -
    Wallet
    -
    - {/* Authorized wallet row */} -
    -
    -

    Authorized wallet

    -

    - Your AntSeed node signs spending requests but never holds USDC or ANTS. - This external wallet can withdraw funds, claim rewards, and close channels - on your behalf — and is your recovery path if you lose access to this node. -

    -
    -
    - {walletAuthorized && operatorDisplay ? ( - <> - {operatorDisplay} - - - - ) : operatorSet === false ? ( - - ) : ( - Loading… - )} -
    + return ( +
    +
    + {/* ── Budget (equal-height with Wallet) ────────────────────── */} +
    +
    - {/* Transfer authorization row */} -
    -
    -

    Transfer authorization

    -

    - Move signing rights to a different wallet you control. Use this if you - want to change which external wallet has operator access. -

    -
    -
    - -
    -
    -
    + {/* ── Wallet (equal-height with Budget) ────────────────────── */} +
    + {/* ── Wallet ─────────────────────────────────────────────── */} +
    Wallet
    +
    + {/* Connected wallet row */} +
    +
    +

    Connected wallet

    +

    + The external wallet used to sign and submit on-chain actions + (withdrawals, claims, channel closes). +

    +
    +
    + {isConnected && connectedAddress ? ( + <> + {truncateAddr(connectedAddress)} + {connector?.name && ( + {connector.name} + )} + + + ) : ( + + {({ openConnectModal, mounted }) => ( + + )} + + )} +
    +
    - {/* ── Network ────────────────────────────────────────────────── */} -
    Network
    -
    - {/* Chain row */} -
    -
    -

    Chain

    -

    Network the AntSeed protocol contracts live on.

    -
    -
    - - {chainLabel} - - - -
    -
    + {/* Authorized wallet row */} +
    +
    +

    Authorized wallet

    +

    + Your AntSeed node signs spending requests but never holds USDC or ANTS. + This external wallet can withdraw funds, claim rewards, and close channels + on your behalf — and is your recovery path if you lose access to this node. +

    +
    +
    + {walletAuthorized && operatorDisplay ? ( + <> + {operatorDisplay} + + + + ) : operatorSet === false ? ( + + ) : ( + Loading… + )} +
    +
    - {/* RPC override row */} -
    -
    -

    RPC endpoint

    -

    - Override the default RPC URL. Changes take effect after reloading the portal. -

    -
    -
    -
    - { setRpcValue(e.target.value); setRpcSaved(false); }} - autoComplete="off" - spellCheck={false} - /> - + {/* Transfer authorization row */} +
    +
    +

    Transfer authorization

    +

    + Move signing rights to a different wallet you control. Use this if you + want to change which external wallet has operator access. +

    +
    +
    + +
    -
    - {/* ── Appearance ─────────────────────────────────────────────── */} -
    Appearance
    -
    -
    -
    -

    Dark theme

    -

    Match AntStation's appearance. Your preference is saved locally.

    -
    -
    - + {/* ── Network (full width) ─────────────────────────────────── */} +
    +
    Network
    +
    + {/* Chain + Block explorer, side by side */} + {explorerUrl ? ( +
    + {chainItem} +
    +
    +

    Block explorer

    +

    View your account and protocol contracts on Basescan.

    +
    + +
    +
    + ) : ( + chainItem + )} + + {/* RPC override — full width with a wide input */} +
    +
    +

    RPC endpoint

    +

    + Override the default RPC URL. Changes take effect after reloading the portal. +

    +
    +
    + { setRpcValue(e.target.value); setRpcSaved(false); }} + autoComplete="off" + spellCheck={false} + /> + +
    +