Skip to content

Global Accounts interactive demo ("See it live")#527

Open
picsoulabs wants to merge 4 commits into
mainfrom
demo/global-accounts-see-it-live
Open

Global Accounts interactive demo ("See it live")#527
picsoulabs wants to merge 4 commits into
mainfrom
demo/global-accounts-see-it-live

Conversation

@picsoulabs
Copy link
Copy Markdown
Contributor

What this is

A first, working pass at an interactive Global Accounts demo for the docs — the neobank/wallet analog of the Flow Builder. It lives at docs.lightspark.com/global-accounts/demo ("See it live", directly under Introduction).

A visitor picks a use case (Fintech/Neobank live; Social + Marketplace stubbed "Soon") and a sign-in method (Passkey / Google / Apple / Email / Phone), then drives a real wallet on a phone — create account → add money → send → cash out → issue a card → tap to pay — by clicking the phone itself, while the exact Grid API calls render in a panel beside it, in sync.

🎯 This is built to be perfected by design, not shipped as-is. Full context for the designer is in components/grid-wallet-demo/HANDOFF.md.

How it's wired (same pattern as the Flow Builder)

  • New standalone Next.js 14 app components/grid-wallet-demo, embedded by the docs in an <iframe> with light/dark postMessage theme sync — identical to how grid-visualizer powers /flow-builder.
  • Same shell + design system: @lightsparkdev/origin tokens, sidebar (475px) + canvas + CodePanel-style API log, central-icons, squircle corners, the EmptyCanvas dotted background, the Header/Footer.
  • Phone wallet ("Aurora") adapted from the bread neobank app; the card is a Robinhood-style dark glass card with a cinematic reveal animation.

Docs changes (mintlify/)

  • global-accounts/demo.mdx — new page (mode: "custom"), iframe + theme-sync, auto-targets localhost:4000 locally / grid-wallet-demo.vercel.app in prod.
  • docs.json — nav entry under Overview (after index) + chrome-hide CSS for #wallet-demo-container.
  • style.css — full-bleed iframe sizing (mirrors #flow-builder-container).

Run locally

cd components/grid-wallet-demo && npm install --ignore-scripts && npm run dev
# http://localhost:4000/?embed=true&theme=dark   (try light too)

⚠️ Before this goes live

  1. Vercel project for components/grid-wallet-demo (build npm run build, install --ignore-scripts for the central-icons license hook, same as grid-visualizer). The docs page is blank until this exists. Swap the URL in demo.mdx if different.
  2. Decide: API calls live against sandbox vs. the current scripted happy path (matches the Flow Builder today).

What's faked (scripted, like the Flow Builder)

  • API calls are illustrative (realistic shapes, not executed) — defined per-action in src/data/actions.ts.
  • Credential screens (OTP / Face ID / Google / Apple) auto-play.
  • Fixed amounts; Social/Marketplace personas are "Soon" stubs.

Designer focus areas (see HANDOFF.md)

The phone is ~90% of the craft (Phone.tsx / Phone.module.scss): per-screen spacing/type, the card art + reveal timing, native-feeling auth sheets, per-persona theming, and a light-mode pass.

🤖 Generated with Claude Code

Interactive, embeddable wallet demo for the Grid docs — the neobank analog of
the Flow Builder. New standalone Next.js app (components/grid-wallet-demo) that
the docs embed in an iframe with light/dark theme sync, mirroring how
grid-visualizer powers /flow-builder.

- Same shell + design system as the Flow Builder (@lightsparkdev/origin,
  sidebar + canvas + CodePanel-style API log, central-icons, squircle).
- Playground model: pick a use case + sign-in method, then drive a real wallet
  on a phone (create account, add money, send, cash out, issue a card, tap to
  pay) by clicking the phone itself; the exact Grid API calls render alongside.
- Phone wallet adapted from the bread neobank app; Robinhood-style glass card
  with a cinematic reveal.
- Scripted happy path (no live sandbox calls), like the Flow Builder.

Docs wiring (mintlify/): new global-accounts/demo.mdx ("See it live", under
Introduction), docs.json nav + chrome-hide CSS, style.css full-bleed iframe.

Deployment + designer handoff notes in components/grid-wallet-demo/HANDOFF.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
grid-flow-builder Ready Ready Preview, Comment Jun 4, 2026 11:31pm

Request Review

@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented May 28, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Grid 🟢 Ready View Preview May 28, 2026, 9:08 PM

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Greptile Summary

This PR introduces a new standalone Next.js 14 interactive demo for Global Accounts (components/grid-wallet-demo), embedded in the Mintlify docs via an iframe with light/dark theme sync — following the same pattern as the existing Flow Builder. The phone UI drives a scripted wallet flow (create account → add money → send → withdraw → issue card → tap to pay) while showing illustrative Grid API calls in a side panel.

  • New grid-wallet-demo app: Full phone simulator with real WebAuthn/Google/Apple/OTP auth ceremonies, animated screens, and a per-action API call log panel.
  • Docs integration (mintlify/): New global-accounts/demo.mdx page (mode: \"custom\"), nav entry in docs.json, and full-bleed CSS in style.css mirroring the flow-builder layout.
  • Auth library (src/lib/auth.ts): Module-level promise singletons for GIS and Apple JS SDK — a network failure on first load permanently caches the rejection, making those sign-in paths irrecoverable without a page reload.

Confidence Score: 3/5

Merging is safe for a not-live-yet preview, but the demo has several functional issues that would frustrate real visitors before fixes land.

The auth library permanently caches rejected GIS/Apple script loads, and the Google sign-in flow can freeze the entire demo from a missing client ID or a blocked CDN. These pair with the previously-flagged balance-guard holes and the total-balance-vs-delta display bug, together affecting most interactive paths a visitor would try.

components/grid-wallet-demo/src/lib/auth.ts (stale rejection cache), components/grid-wallet-demo/src/data/actions.ts (balance guards), components/grid-wallet-demo/src/components/Phone.tsx (Google auth freeze and change-row display)

Important Files Changed

Filename Overview
components/grid-wallet-demo/src/lib/auth.ts Real WebAuthn/GIS/Apple auth ceremonies; gisPromise/applePromise are module-level singletons that permanently cache rejected promises, blocking those sign-in paths after one network failure.
components/grid-wallet-demo/src/app/page.tsx Main orchestration: interactive prompts, wallet state machine, action dispatch. runSend/runWithdraw guard with Math.max(0,...) prevents negative balance in state, but action available guards in actions.ts still allow firing when balance is less than deduction amount.
components/grid-wallet-demo/src/data/actions.ts Action guard definitions; send/withdraw guards allow firing when balance is less than deduction (previously flagged), tap has no minimum-balance guard (previously flagged).
components/grid-wallet-demo/src/components/Phone.tsx Phone UI; changeRow displays total balance as today's delta (previously flagged), Google sign-in freezes demo when NEXT_PUBLIC_GOOGLE_CLIENT_ID is unset (previously flagged).
mintlify/global-accounts/demo.mdx Docs page embedding the iframe; postMessage handler processes theme-sync without validating e.origin (previously flagged).
components/grid-wallet-demo/next.config.mjs Next.js config; ignoreBuildErrors:true swallows all TS errors (previously flagged); CSP frame-ancestors correctly enumerates allowed embedding origins.
components/grid-wallet-demo/src/hooks/useTheme.ts Theme sync hook; posts theme-request and theme-sync messages to * instead of narrowing to the known docs origin.

Sequence Diagram

sequenceDiagram
    participant User
    participant Docs as docs.lightspark.com
    participant IFrame as grid-wallet-demo iframe
    participant Phone as Phone UI
    participant API as API Panel

    User->>Docs: Loads /global-accounts/demo
    Docs->>IFrame: "Embed /?embed=true&theme=dark"
    IFrame->>Docs: postMessage theme-request to any origin
    Docs->>IFrame: postMessage theme-sync theme
    User->>IFrame: Picks persona + sign-in method
    User->>Phone: Clicks Sign in
    Phone->>IFrame: onAction create
    IFrame->>Phone: Show auth screen
    Phone->>IFrame: Auth resolved
    IFrame->>API: Push credential and account API calls
    IFrame->>Phone: wallet.created true
    User->>Phone: Clicks Add money
    Phone->>IFrame: onAction add
    IFrame->>Phone: AmountEntryScreen
    Phone->>IFrame: submitAmount dollars
    IFrame->>API: Push addMoneyCalls cents
    IFrame->>Phone: Update balance
    User->>Phone: Clicks Issue a card
    Phone->>IFrame: onAction card
    IFrame->>Phone: CardReveal animation
    User->>Phone: Clicks Tap to pay
    Phone->>IFrame: onAction tap
    IFrame->>API: Push transaction API call
    IFrame->>Phone: TapScreen activated
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
components/grid-wallet-demo/src/lib/auth.ts:11-23
**Rejected GIS promise is permanently cached, permanently blocking Google sign-in**

`gisPromise` is a module-level singleton. If `loadGis()` rejects (GIS script blocked by an ad blocker, network hiccup, etc.), `gisPromise` is set to a rejected `Promise` — which is truthy. Every subsequent call to `loadGis()` hits the `if (gisPromise) return gisPromise` guard and immediately returns the same rejected promise, so there is no retry possible for the rest of the session.

In `GoogleSignInScreen` the rejection is swallowed by `.catch(() => {})`, leaving the GIS button never rendered. The `googlePrompt` promise created in `promptGoogle` never resolves, `running` stays `true`, and the demo locks up with no escape. Ad blockers blocking `accounts.google.com` are extremely common on developer-audience docs pages, making this trigger realistic. The same pattern affects `applePromise`/`loadAppleAuth`.

### Issue 2 of 2
components/grid-wallet-demo/src/hooks/useTheme.ts:49-52
The iframe posts messages to `'*'`, meaning any embedding origin receives them. The outbound `theme-sync` message (line 63) also uses `'*'`. Since the `Content-Security-Policy` in `next.config.mjs` already enumerates the exact set of allowed embedding origins, the `targetOrigin` here can be narrowed to `https://docs.lightspark.com` (and `http://localhost:*` for local dev) to avoid broadcasting to an arbitrary parent.

```suggestion
    // Ask the parent (if embedded) for the current theme.
    if (window.parent && window.parent !== window) {
      window.parent.postMessage({ type: 'theme-request' }, 'https://docs.lightspark.com');
    }
```

Reviews (4): Last reviewed commit: "Enable Apple sign-in in Global Accounts ..." | Re-trigger Greptile

Comment on lines +88 to +93
id: 'tap',
label: 'Tap to pay',
desc: 'Spend at a store',
icon: 'tap',
available: (s) => s.hasCard,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 tap action can produce a negative wallet balance

The tap action's available guard only checks s.hasCard — there is no minimum balance requirement. A user can: (1) create an account, (2) immediately issue a card (no funds required), (3) tap → balance becomes -$7.32. With funds, repeated tapping also drives the balance negative because the same guard allows unlimited taps. The UI then displays a negative balance in the demo, which misrepresents what the Grid API would actually permit.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-demo/src/data/actions.ts
Line: 88-93

Comment:
**`tap` action can produce a negative wallet balance**

The `tap` action's `available` guard only checks `s.hasCard` — there is no minimum balance requirement. A user can: (1) create an account, (2) immediately issue a card (no funds required), (3) tap → balance becomes -$7.32. With funds, repeated tapping also drives the balance negative because the same guard allows unlimited taps. The UI then displays a negative balance in the demo, which misrepresents what the Grid API would actually permit.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +238 to +243
{hasFunds && (
<div className={styles.changeRow}>
<span className={styles.deltaUp}>+ {phone.balance}</span>
<span className={styles.deltaChip}>Today</span>
</div>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 changeRow displays total balance instead of the day's change amount

phone.balance holds the total wallet balance, not the delta from the last transaction. After a second add action the row would render + $10,000.00 · Today even though only $5,000 was added, misleading visitors about what the display represents.

Suggested change
{hasFunds && (
<div className={styles.changeRow}>
<span className={styles.deltaUp}>+ {phone.balance}</span>
<span className={styles.deltaChip}>Today</span>
</div>
)}
{hasFunds && (
<div className={styles.changeRow}>
<span className={styles.deltaUp}>+ $5,000.00</span>
<span className={styles.deltaChip}>Today</span>
</div>
)}
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-demo/src/components/Phone.tsx
Line: 238-243

Comment:
**`changeRow` displays total balance instead of the day's change amount**

`phone.balance` holds the *total* wallet balance, not the delta from the last transaction. After a second `add` action the row would render `+ $10,000.00 · Today` even though only $5,000 was added, misleading visitors about what the display represents.

```suggestion
        {hasFunds && (
          <div className={styles.changeRow}>
            <span className={styles.deltaUp}>+ $5,000.00</span>
            <span className={styles.deltaChip}>Today</span>
          </div>
        )}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +29 to +43
if (e.data && e.data.type === 'theme-request') {
sendTheme();
return;
}
if (e.data && e.data.type === 'theme-sync') {
const isDark = document.documentElement.classList.contains('dark');
const wantsDark = e.data.theme === 'dark';
if (isDark !== wantsDark) {
ignoreNextMutation = true;
document.documentElement.classList.toggle('dark');
}
}
};
window.addEventListener('message', handleMessage);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security postMessage handler trusts messages from any origin

handleMessage processes both theme-request and theme-sync events without validating e.origin. Any page that holds a reference to the docs window can send { type: 'theme-sync', theme: 'dark' } and toggle the dark-mode class on the HTML element. The fix is a simple origin allowlist — e.g. skip messages unless e.origin === window.location.origin || e.origin.endsWith('.lightspark.com').

Prompt To Fix With AI
This is a comment left during a code review.
Path: mintlify/global-accounts/demo.mdx
Line: 29-43

Comment:
**`postMessage` handler trusts messages from any origin**

`handleMessage` processes both `theme-request` and `theme-sync` events without validating `e.origin`. Any page that holds a reference to the docs window can send `{ type: 'theme-sync', theme: 'dark' }` and toggle the dark-mode class on the HTML element. The fix is a simple origin allowlist — e.g. skip messages unless `e.origin === window.location.origin || e.origin.endsWith('.lightspark.com')`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +10 to +14
typescript: {
// Origin is source-linked without its own node_modules,
// so its transitive type imports can't resolve from ../origin
ignoreBuildErrors: true,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 TypeScript build errors are silently swallowed

ignoreBuildErrors: true means next build will succeed even when there are type errors. The comment explains the current need (transitive imports from the local @lightsparkdev/origin source link), but it also masks any new type mistakes in this project's own code. Consider scoping the ignore to only the problematic external package paths once the origin package situation is resolved, or at minimum running tsc --noEmit in CI as a separate check so regressions don't go unnoticed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-demo/next.config.mjs
Line: 10-14

Comment:
**TypeScript build errors are silently swallowed**

`ignoreBuildErrors: true` means `next build` will succeed even when there are type errors. The comment explains the current need (transitive imports from the local `@lightsparkdev/origin` source link), but it also masks any new type mistakes in this project's own code. Consider scoping the ignore to only the problematic external package paths once the origin package situation is resolved, or at minimum running `tsc --noEmit` in CI as a separate check so regressions don't go unnoticed.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

- demo.mdx: remove JS line comment that broke Mintlify's MDX parse
  (rendered the page blank) and add phone icon to "See it live"
- add phone.svg sidebar icon
- vendor Suisse Intl fonts into public/fonts so Origin's @font-face
  resolves (was 404ing, falling back to system fonts)
- flow.ts: keep Google/Apple capitalized in the sign-in CTA
- Sidebar.tsx: mark Phone (SMS) sign-in as "Soon"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +70 to +77
available: (s) => s.created && s.balanceCents > 0,
},
{
id: 'withdraw',
label: 'Withdraw',
desc: 'Cash out to a bank',
icon: 'bank',
available: (s) => s.created && s.balanceCents > 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 send and withdraw guards allow negative balance

Both guards check only s.balanceCents > 0, but each deducts a fixed amount that can exceed the available balance. A visitor who mixes actions can reach a small positive balance that still enables these buttons: add ($5,000) → send ×19 ($4,750 total) → balance $250 → withdraw ($200) → balance $50 → send is still available (50 > 0) and deducts $25,000 cents → display shows −$249.50. Like the already-noted tap issue, the fix is to require s.balanceCents >= SEND and s.balanceCents >= WITHDRAW in the respective guards.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-demo/src/data/actions.ts
Line: 70-77

Comment:
**`send` and `withdraw` guards allow negative balance**

Both guards check only `s.balanceCents > 0`, but each deducts a fixed amount that can exceed the available balance. A visitor who mixes actions can reach a small positive balance that still enables these buttons: `add` ($5,000) → `send` ×19 ($4,750 total) → balance $250 → `withdraw` ($200) → balance $50 → `send` is still available (`50 > 0`) and deducts $25,000 cents → display shows **−$249.50**. Like the already-noted `tap` issue, the fix is to require `s.balanceCents >= SEND` and `s.balanceCents >= WITHDRAW` in the respective guards.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +362 to +391
useEffect(() => {
let cancelled = false;
loadGis()
.then(() => {
if (cancelled || !containerRef.current) return;
const google = (window as any).google;
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
google.accounts.id.initialize({
client_id: clientId,
nonce,
callback: (resp: { credential?: string }) => {
if (resp?.credential && !fired.current) {
fired.current = true;
onCredential(resp.credential);
}
},
});
google.accounts.id.renderButton(containerRef.current, {
theme: 'filled_blue',
size: 'large',
type: 'standard',
text: 'continue_with',
shape: 'pill',
});
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [nonce, onCredential]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Google OAuth silently freezes the entire demo

google.accounts.id.initialize({ client_id: clientId, ... }) is called with clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID. If that env var is unset, GIS either throws or renders a non-functional button; both outcomes are swallowed by .catch(() => {}). The promptGoogle promise returned by authenticate() then never resolves — running stays true and, because wallet.created is still false at this stage, neither sidebar action buttons nor the "Start over" button are accessible. The user has no escape short of reloading the page. A guard that rejects the googlePrompt early (or surfaces a visible error) when clientId is falsy would prevent the lock-up.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-demo/src/components/Phone.tsx
Line: 362-391

Comment:
**Google OAuth silently freezes the entire demo**

`google.accounts.id.initialize({ client_id: clientId, ... })` is called with `clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID`. If that env var is unset, GIS either throws or renders a non-functional button; both outcomes are swallowed by `.catch(() => {})`. The `promptGoogle` promise returned by `authenticate()` then never resolves — `running` stays `true` and, because `wallet.created` is still `false` at this stage, neither sidebar action buttons nor the "Start over" button are accessible. The user has no escape short of reloading the page. A guard that rejects the `googlePrompt` early (or surfaces a visible error) when `clientId` is falsy would prevent the lock-up.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +11 to +23
let gisPromise: Promise<void> | null = null;
export function loadGis(): Promise<void> {
if (gisPromise) return gisPromise;
gisPromise = new Promise((resolve, reject) => {
if ((window as any).google?.accounts?.id) return resolve();
const s = document.createElement('script');
s.src = 'https://accounts.google.com/gsi/client';
s.async = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error('Failed to load Google Identity Services.'));
document.head.appendChild(s);
});
return gisPromise;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Rejected GIS promise is permanently cached, permanently blocking Google sign-in

gisPromise is a module-level singleton. If loadGis() rejects (GIS script blocked by an ad blocker, network hiccup, etc.), gisPromise is set to a rejected Promise — which is truthy. Every subsequent call to loadGis() hits the if (gisPromise) return gisPromise guard and immediately returns the same rejected promise, so there is no retry possible for the rest of the session.

In GoogleSignInScreen the rejection is swallowed by .catch(() => {}), leaving the GIS button never rendered. The googlePrompt promise created in promptGoogle never resolves, running stays true, and the demo locks up with no escape. Ad blockers blocking accounts.google.com are extremely common on developer-audience docs pages, making this trigger realistic. The same pattern affects applePromise/loadAppleAuth.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/grid-wallet-demo/src/lib/auth.ts
Line: 11-23

Comment:
**Rejected GIS promise is permanently cached, permanently blocking Google sign-in**

`gisPromise` is a module-level singleton. If `loadGis()` rejects (GIS script blocked by an ad blocker, network hiccup, etc.), `gisPromise` is set to a rejected `Promise` — which is truthy. Every subsequent call to `loadGis()` hits the `if (gisPromise) return gisPromise` guard and immediately returns the same rejected promise, so there is no retry possible for the rest of the session.

In `GoogleSignInScreen` the rejection is swallowed by `.catch(() => {})`, leaving the GIS button never rendered. The `googlePrompt` promise created in `promptGoogle` never resolves, `running` stays `true`, and the demo locks up with no escape. Ad blockers blocking `accounts.google.com` are extremely common on developer-audience docs pages, making this trigger realistic. The same pattern affects `applePromise`/`loadAppleAuth`.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants