diff --git a/app/vibenet/demos/account/AccountDemo.tsx b/app/vibenet/demos/account/AccountDemo.tsx
index 54f6347..9b40653 100644
--- a/app/vibenet/demos/account/AccountDemo.tsx
+++ b/app/vibenet/demos/account/AccountDemo.tsx
@@ -30,7 +30,6 @@ import { useEffect, useMemo, useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { Button } from '../../../components/ui/Button';
-import { Card } from '../../../components/ui/Card';
import { cn } from '../../../components/ui/cn';
import { CloseIcon } from '../../../components/ui/icons';
import { Modal } from '../../../components/ui/Modal';
@@ -48,6 +47,7 @@ import { AccountSwitcher } from '../_shared/AccountSwitcher';
import { AddressAutocomplete, type AddressBookEntry } from '../_shared/AddressAutocomplete';
import { ActivityLog } from './components/ActivityLog';
import { AppCard, AppCardPlaceholder, AppsNetworkNotice } from './components/AppsView';
+import { FeatureGridCard, FeatureGridPlaceholder } from './components/FeatureGridCard';
import { Badge, CheckIcon, KindBadge } from '../_shared/primitives';
import { DEMO_APPS, type DemoApp } from './library/apps';
import { DEMO_CHAINS, estimateTxGas, PAYER_URL } from './library/chains';
@@ -241,6 +241,7 @@ export function AccountDemo() {
extraChanges: string[] = [],
) => {
setResult({ serialized, txHash, by: by.label, kind: by.kind, pending, gasNote });
+ toast.success(pending ? 'Submitted — awaiting confirmation' : 'Transaction landed onchain');
pushActivity({
kind: a.deployed && !pending ? 'transact' : 'create',
txHash,
@@ -278,16 +279,21 @@ export function AccountDemo() {
...pendingScope.map((o) => `scope ${o.label} → ${scopeLabel(o.toScope)}`),
];
+ const surfaceSendError = (message: string) => {
+ setError(message);
+ toast.error(message);
+ };
+
// Transact: native offline sign, own ETH gas.
const doSignNative = async () => {
if (!acct || !txSigner || !callsValid) return;
const sessionPolicy = activeSessionKey?.policy;
if (txIsSession && gasMode !== 'free') {
- setError('Session keys can only send sponsored (free) transactions. Switch gas to Sponsored.');
+ surfaceSendError('Session keys can only send sponsored (free) transactions. Switch gas to Sponsored.');
return;
}
if (txIsSession && !acct.deployed && !activeSessionKey?.pendingAuth) {
- setError('Authorize this session key with an owner key first (Apply now).');
+ surfaceSendError('Authorize this session key with an owner key first (Apply now).');
return;
}
setSigning(true);
@@ -335,7 +341,7 @@ export function AccountDemo() {
} catch (err) {
if (handleSeqMismatch(err, seqCtx)) return false;
const e = err as { message?: string; name?: string };
- setError(conciseError(e.name === 'NotAllowedError' ? 'Signature was dismissed.' : (e.message ?? String(err))));
+ surfaceSendError(conciseError(e.name === 'NotAllowedError' ? 'Signature was dismissed.' : (e.message ?? String(err))));
return false;
} finally {
setSigning(false);
@@ -352,11 +358,11 @@ export function AccountDemo() {
if (!acct || !txSigner || !callsValid) return;
const sessionPolicy = activeSessionKey?.policy;
if (txIsSession && gasMode !== 'free') {
- setError('Session keys can only send sponsored (free) transactions. Switch gas to Sponsored.');
+ surfaceSendError('Session keys can only send sponsored (free) transactions. Switch gas to Sponsored.');
return;
}
if (txIsSession && !acct.deployed && !activeSessionKey?.pendingAuth) {
- setError('Authorize this session key with an owner key first (Apply now).');
+ surfaceSendError('Authorize this session key with an owner key first (Apply now).');
return;
}
setSigning(true);
@@ -453,7 +459,7 @@ export function AccountDemo() {
if (handleSeqMismatch(err, seqCtx)) return false;
const e = err as { message?: string; name?: string };
const msg = e.message ?? String(err);
- setError(
+ surfaceSendError(
conciseError(
e.name === 'NotAllowedError'
? 'Signature was dismissed.'
@@ -700,7 +706,9 @@ export function AccountDemo() {
{FEATURES.map((feature) => (
))}
- Features
+
+ Features
+
{/* Transact + the app cards, as peers in one grid. `initial={false}`:
the crossfade is for switching accounts, not first paint. */}
@@ -722,7 +730,7 @@ export function AccountDemo() {
- {error && !estimateBlocked ? (
+ {error && !estimateBlocked && !transactModalOpen ? (
- Advanced Transactions
- Create and select an account to transact.
-
+
);
return (
<>
-
-
-
-
-
- Advanced Transactions
-
-
- Compose and send EIP-8130 transactions from your account.
-
-
-
-
-
+
+
+
+ }
+ title="Advanced Transactions"
+ description="Compose and send EIP-8130 transactions from your account."
+ >
+
+
- Sponsorship
- Create and select an account to send a sponsored transaction.
-
- );
+ if (!acct) {
+ return (
+
+ );
+ }
return (
-
-
-
-
-
- Sponsorship
-
-
- Send a transaction with its gas paid by a payer — no ETH required in your account.
-
-
-
-
-
+
+
+
+
+ }
+ title="Sponsorship"
+ description="Send a transaction with its gas paid by a payer — no ETH required in your account."
+ >
+
+
);
}
function renderOwners() {
- if (!acct) return (
-
- Modify Owners
- Create and select an account to manage owners.
-
- );
+ if (!acct) {
+ return (
+
+ );
+ }
return (
-
-
-
-
-
- Modify Owners
-
-
- Add or revoke owner keys and rotate signers — swap keys anytime without ever migrating
- accounts.
-
-
-
-
-
+
+
+
+
+ }
+ title="Modify Owners"
+ description="Add or revoke owner keys and rotate signers — swap keys anytime without ever migrating accounts."
+ >
+
+
);
}
function renderBatchedCalls() {
- if (!acct) return (
-
- Batched Calls
- Create and select an account to send a batched transaction.
-
- );
+ if (!acct) {
+ return (
+
+ );
+ }
return (
-
-
-
-
-
- Batched Calls
-
-
- Send multiple actions in one transaction — e.g. approve and swap — so they either all land
- together or none do.
-
-
-
-
-
+
+
+
+
+
+ }
+ title="Batched Calls"
+ description="Send multiple actions in one transaction — e.g. approve and swap — so they either all land together or none do."
+ >
+
+
);
}
function renderGasToken() {
- if (!acct) return (
-
- Pay Gas in Any Token
- Create and select an account to pay gas in a token.
-
- );
+ if (!acct) {
+ return (
+
+ );
+ }
return (
-
-
-
-
-
- Pay Gas in Any Token
-
-
- Pay gas with any supported token including stablecoins — settle a batched transaction's
- fees in USDV with no ETH in your account.
-
-
-
-
-
+
+
+
+
+ }
+ title="Pay Gas in Any Token"
+ description="Pay gas with any supported token including stablecoins — settle a batched transaction's fees in USDV with no ETH in your account."
+ >
+
+
);
}
}
@@ -1745,9 +1717,8 @@ type SubmittedResult = {
} | null;
// Third stage of the Transact modal: shown once "Send" is pressed. Renders the
-// in-flight signing/broadcast status, then whichever of success or error the
-// send actually resolved to — this is the single place a sent transaction's
-// outcome is surfaced (no toasts).
+// in-flight signing/broadcast status, then success or error. Outcomes also
+// fire a sonner toast so the result is visible if the modal is dismissed.
function SubmittedBody({
signing,
submitStatus,
diff --git a/app/vibenet/demos/account/components/AppsView.tsx b/app/vibenet/demos/account/components/AppsView.tsx
index 754eee0..d433b22 100644
--- a/app/vibenet/demos/account/components/AppsView.tsx
+++ b/app/vibenet/demos/account/components/AppsView.tsx
@@ -1,13 +1,14 @@
'use client';
import { Button } from '../../../../components/ui/Button';
-import { Card } from '../../../../components/ui/Card';
-import { Text } from '../../../../components/ui/Text';
import type { DemoApp } from '../library/apps';
-import type { DemoChain } from '../library/chains';
import { formatExpiry, type AppSessionKey, type AppSubAccount, type StoredAccount } from '../library/model';
import { short, type WalletSigner } from '../shared';
import { Badge } from '../../_shared/primitives';
+import { FeatureGridCard, FeatureGridPlaceholder } from './FeatureGridCard';
+
+const CONNECTED_FOOTER =
+ 'border-t border-bds-gray-10 pt-3 dark:border-white/10';
// Banner shown above the app cards when the active network doesn't support
// native EIP-8130 grants yet — apps stay disabled until the user switches.
@@ -44,113 +45,124 @@ type AppCardProps = {
deleteVault: (sub: AppSubAccount) => void;
};
+function AppIcon({ app }: { app: DemoApp }) {
+ if (app.id === 'monthly-vibes') {
+ return (
+
+ );
+ }
+ if (app.id === 'spending-account') {
+ return (
+
+ );
+ }
+ return {app.emoji};
+}
+
// A single demo app: connects to the active account through a scoped grant it
// fully controls — a capped session key (subscriptions) or a delegated
-// sub-account. Ported from the source `renderApps`.
+// sub-account. Idle chrome matches the other Features tiles; once a grant is
+// live the footer becomes a status row with a divider.
export function AppCard(p: AppCardProps) {
- const { acct, app } = p;
+ const { app } = p;
const sk = app.grant === 'session' ? p.sessionKeyFor(app.name) : undefined;
const sub = app.grant === 'subaccount' ? p.subAccountFor(app.name) : undefined;
const connecting = p.appBusy === app.id;
- return (
-
-