From b04ee1eb52e6e3d1e24c498d2f440c1df41e3292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Wed, 3 Jun 2026 15:32:47 +0100 Subject: [PATCH 1/3] =?UTF-8?q?Portal:=20UX=20polish=20=E2=80=94=20onboard?= =?UTF-8?q?ing,=20account=20pill,=20Settings,=20type=20scale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refinement pass over the buyer Portal on top of the existing redesign. Type & headers - Bump base font 14px → 16px and page title to 24px for a clearer hierarchy. - Drop duplicated per-view page titles; the sticky TopBar is the single source. Account & wallet - Replace the twin top-right buttons with one web3-style account pill (balance + signer address) that opens the wallet drawer; remove the redundant top Deposit button. - WalletDrawer becomes pure account management (no Deposit/Withdraw). - Move the theme toggle to a larger top-right icon button; drop the duplicated signer address from the sidebar footer. Onboarding - Retire the full-width "not recoverable" banner. The funds-at-risk warning now rides on the account pill (every tab) and an amber, non-dismissable Get Started state. - Rebuild Get Started as a minimal single-row banner: next step + progress dots + session-only dismiss (returns while setup is incomplete). - Auto-show the "How AntSeed works" explainer once for first-timers and via a new ?welcome URL param; drop the redundant first-run deposit overlay. - Redesign the explainer into a compact horizontal 3-up layout (wide modal variant), no tag pills. Deposit - Fix the misleading "credit limit" message: check wallet balance first and only gate on a real positive credit limit (the unset 0 no longer triggers it). Settings - Two-column layout: Budget | Wallet (equal height) with Network full-width; Network splits into Chain | Block explorer side-by-side and a full-width RPC field (no more truncation). Add a Basescan link. - Redesign the budget cap / low-balance inputs into a single input group and fix the low-balance alert overflowing its column. - Add Connected wallet + Disconnect; remove the redundant Appearance section. Nav - New left-nav icons: grid (Overview), gift (Rewards), sliders (Settings). Co-Authored-By: Claude Opus 4.8 --- apps/payments/web/src/App.tsx | 83 +-- .../web/src/components/BudgetSection.scss | 76 ++- .../web/src/components/BudgetSection.tsx | 20 +- .../web/src/components/DepositView.scss | 4 +- .../web/src/components/DepositView.tsx | 27 +- .../web/src/components/GettingStarted.tsx | 190 +++++++ .../web/src/components/HowItWorksModal.tsx | 144 +++++ apps/payments/web/src/layout/ActionModal.tsx | 2 +- .../web/src/layout/AuthorizeWalletAlert.tsx | 29 - .../web/src/layout/EmptyStateOverlay.tsx | 128 +---- apps/payments/web/src/layout/Sidebar.tsx | 102 +--- apps/payments/web/src/layout/TopBar.tsx | 95 +++- apps/payments/web/src/layout/WalletDrawer.tsx | 21 - apps/payments/web/src/styles/global.scss | 507 ++++++++++++------ apps/payments/web/src/views/ActivityView.tsx | 4 +- apps/payments/web/src/views/OverviewView.tsx | 13 +- apps/payments/web/src/views/RewardsView.scss | 4 +- apps/payments/web/src/views/RewardsView.tsx | 6 +- apps/payments/web/src/views/SettingsView.scss | 92 +++- apps/payments/web/src/views/SettingsView.tsx | 420 +++++++-------- 20 files changed, 1224 insertions(+), 743 deletions(-) create mode 100644 apps/payments/web/src/components/GettingStarted.tsx create mode 100644 apps/payments/web/src/components/HowItWorksModal.tsx delete mode 100644 apps/payments/web/src/layout/AuthorizeWalletAlert.tsx 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) { @@ -94,9 +97,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 */} -
    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 ── */}
    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} + /> + +
    +
    From d04e4f81a9ca1b311130bcd54ba6d70842757ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Wed, 3 Jun 2026 15:47:20 +0100 Subject: [PATCH 2/3] Portal: green Connect-wallet button on Rewards DIEM section The DIEM staking connect prompt used RainbowKit's default (blue), unlike the rest of the app. Swap it for ConnectButton.Custom with the portal's btn-primary, and size it to content (btn-primary is full-width by default) so it's a contained button rather than a full-width slab. Co-Authored-By: Claude Opus 4.8 --- apps/payments/web/src/views/RewardsView.scss | 4 ++++ apps/payments/web/src/views/RewardsView.tsx | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/payments/web/src/views/RewardsView.scss b/apps/payments/web/src/views/RewardsView.scss index 8e265da4e..52302d57b 100644 --- a/apps/payments/web/src/views/RewardsView.scss +++ b/apps/payments/web/src/views/RewardsView.scss @@ -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 { diff --git a/apps/payments/web/src/views/RewardsView.tsx b/apps/payments/web/src/views/RewardsView.tsx index b8432b9f8..bfafce300 100644 --- a/apps/payments/web/src/views/RewardsView.tsx +++ b/apps/payments/web/src/views/RewardsView.tsx @@ -814,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 }) => ( + + )} +
    )} From b0e307a2c758697d4f8948f4f62d115f3459df59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Sat, 6 Jun 2026 17:56:26 +0100 Subject: [PATCH 3/3] Portal: Overview headline shows available balance, reserved below The hero figure was labeled "Available balance" but rendered the total. Show the actual available amount as the headline, with the reserved amount on the note line below (hidden when nothing is reserved), and drop the now-redundant "$X available" text. Remove the unused total/totalUsd locals. Co-Authored-By: Claude Opus 4.8 --- apps/payments/web/src/views/OverviewView.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/payments/web/src/views/OverviewView.tsx b/apps/payments/web/src/views/OverviewView.tsx index 0be715e50..1e1fd2870 100644 --- a/apps/payments/web/src/views/OverviewView.tsx +++ b/apps/payments/web/src/views/OverviewView.tsx @@ -75,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; @@ -129,9 +127,9 @@ export function OverviewView({ <>
    Available balance
    - {totalUsd !== null ? ( + {availableUsd !== null ? ( <> - ${totalUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{' '} + ${availableUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}{' '} USDC ) : ( @@ -139,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}