Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 46 additions & 37 deletions apps/payments/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ 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';
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<TabId>([
Expand All @@ -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);
Expand All @@ -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<BalanceData | null>(null);
const [balanceLoaded, setBalanceLoaded] = useState(false);
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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 (
<>
<div className={`dash-shell${shellBlurred ? ' dash-shell--blurred' : ''}`}>
<Sidebar
activeTab={activeTab}
onSelect={onSelectTab}
isDark={isDark}
onToggleTheme={onToggleTheme}
walletAddress={shortAddr}
walletAuthorized={isAuthorized}
onOpenWallet={onOpenWalletDrawer}
/>
<Sidebar activeTab={activeTab} onSelect={onSelectTab} />
<div className="dash-main">
<TopBar
activeTab={activeTab}
balance={balance}
buyerEvmAddress={buyerEvmAddress}
atRisk={unauthorizedAtRisk}
isDark={isDark}
onToggleTheme={onToggleTheme}
onOpenWallet={onOpenWalletDrawer}
onOpenDeposit={onOpenDeposit}
/>
<AuthorizeWalletAlert />
<main className="dash-content">
{/* New 4-item portal nav */}
{(activeTab === 'overview' || activeTab === 'dashboard') && (
Expand All @@ -263,6 +276,7 @@ function AppShell({
config={config}
onOpenDeposit={onOpenDeposit}
onOpenWithdraw={onOpenWithdraw}
onOpenHowItWorks={() => setHowItWorksOpen(true)}
onGoToChannels={goToChannels}
onGoToActivity={goToActivity}
onGoToRewards={goToRewards}
Expand All @@ -288,20 +302,10 @@ function AppShell({
balance={balance}
config={config}
buyerEvmAddress={buyerEvmAddress}
onOpenDeposit={onOpenDeposit}
onOpenWithdraw={onOpenWithdraw}
/>
</div>
<LoaderOverlay isVisible={isLoading} />
<EmptyStateOverlay
phase={overlayPhase}
config={config}
balance={balance}
buyerAddress={buyerEvmAddress}
onDeposited={handleDeposited}
onContinue={dismissSuccess}
onDismissDeposit={dismissDepositPrompt}
/>
<EmptyStateOverlay phase={overlayPhase} onContinue={dismissSuccess} />
<ActionModal
isOpen={actionModal === 'deposit'}
onClose={onCloseActionModal}
Expand All @@ -324,6 +328,11 @@ function AppShell({
>
<WithdrawView config={config} balance={balance} onAction={refreshBalance} />
</ActionModal>
<HowItWorksModal
isOpen={howItWorksOpen}
onClose={() => setHowItWorksOpen(false)}
onOpenDeposit={onOpenDeposit}
/>
</>
);
}
76 changes: 52 additions & 24 deletions apps/payments/web/src/components/BudgetSection.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 ── */
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down
20 changes: 9 additions & 11 deletions apps/payments/web/src/components/BudgetSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,13 @@ function BudgetMeter({
<div className={meterClass} style={{ width: `${pct}%` }} />
</div>
</>
) : (
<span className="budget-unset">No cap set</span>
)}
) : null}

{/* Cap input */}
<div className="budget-cap-row">
<span className="budget-cap-prefix">$</span>
<div className="budget-field">
<span className="budget-field-prefix">$</span>
<input
className="set-input budget-cap-input"
className="budget-field-input"
type="number"
min="0"
step="1"
Expand All @@ -143,7 +141,7 @@ function BudgetMeter({
/>
<button
type="button"
className="set-btn-ghost"
className="budget-field-btn"
onClick={handleCapSave}
disabled={!capDraft}
>
Expand Down Expand Up @@ -244,10 +242,10 @@ function LowBalanceRow({
</button>
</div>
)}
<div className="budget-cap-row">
<span className="budget-cap-prefix">$</span>
<div className="budget-field">
<span className="budget-field-prefix">$</span>
<input
className="set-input budget-cap-input"
className="budget-field-input"
type="number"
min="0"
step="1"
Expand All @@ -259,7 +257,7 @@ function LowBalanceRow({
/>
<button
type="button"
className="set-btn-ghost"
className="budget-field-btn"
onClick={handleSave}
disabled={!draft}
>
Expand Down
4 changes: 2 additions & 2 deletions apps/payments/web/src/components/DepositView.scss
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
.dv-step--done & {
background: var(--accent);
border-color: var(--accent);
color: var(--accent-text);
color: var(--on-accent);
}

.dv-step--active & {
Expand All @@ -229,7 +229,7 @@
width: 100%;
padding: var(--sp-3) var(--sp-4);
background: var(--accent);
color: var(--accent-text);
color: var(--on-accent);
border: none;
border-radius: var(--radius-md);
font: inherit;
Expand Down
Loading