Global Accounts interactive demo ("See it live")#527
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
Greptile SummaryThis PR adds a new standalone Next.js 14 app (
Confidence Score: 5/5Safe to merge — this is a self-contained docs demo component with no production data paths; all API calls are illustrative. The change adds a new standalone Next.js app and a docs page. No existing code is modified in a breaking way, and the demo's illustrative API calls carry no real side effects. The two new findings (dead AmountScreen branches and uncapped amount entry) are cosmetic inconsistencies in a scripted happy-path demo — neither affects real user data or blocks the primary flow. The auth singleton caching in src/lib/auth.ts and the silent Google sign-in freeze in Phone.tsx (both tracked in existing threads) are the most impactful issues for developer-audience visitors who commonly have ad blockers.
|
| Filename | Overview |
|---|---|
| components/grid-wallet-demo/src/app/page.tsx | Main page orchestrates the interactive demo; add/send/withdraw use dedicated async handlers with Math.max(0,...) balance clamping, but card/tap still go through the old runAction path. Amount entry has no upper-bound guard. |
| components/grid-wallet-demo/src/components/Phone.tsx | Phone UI renders correctly for all active flows; AmountScreen (non-interactive) mapped to addmoney/send/withdraw screen IDs in the render switch is unreachable dead code since those transient states are never set. |
| components/grid-wallet-demo/src/lib/auth.ts | GIS/Apple promise singletons cache rejections permanently — once blocked by an ad blocker or network error, sign-in stays broken for the session (flagged in existing thread). |
| components/grid-wallet-demo/src/data/actions.ts | Action definitions and runAction are only used for card/tap in the new flow; add/send/withdraw go through dedicated handlers in page.tsx. |
| components/grid-wallet-demo/src/data/apiCalls.ts | Illustrative Grid API call shapes; unit conventions differ correctly between USD (cents) for add-money and USDB (1e6 sub-units) for send/withdraw. |
| mintlify/global-accounts/demo.mdx | Iframe embed page; postMessage handler lacks origin validation (flagged in existing thread). Correctly sets iframe allow attributes for passkey/clipboard. |
| components/grid-wallet-demo/next.config.mjs | CSP frame-ancestors correctly restricts embedders. ignoreBuildErrors: true suppresses TypeScript errors (flagged in existing thread). |
| components/grid-wallet-demo/src/hooks/useTheme.ts | Theme hook matches the grid-visualizer contract; sends theme-request on mount and syncs via postMessage. Outgoing messages use '*' target origin, exposing only non-sensitive theme data. |
Sequence Diagram
sequenceDiagram
participant Docs as docs.lightspark.com
participant IFrame as grid-wallet-demo iframe
participant Phone as Phone (UI)
participant Sidebar as Sidebar (UI)
participant ApiPanel as API Steps Panel
Docs->>IFrame: "load src=/?embed=true&theme=dark"
IFrame->>Docs: "postMessage { type: 'theme-request' }"
Docs->>IFrame: "postMessage { type: 'theme-sync', theme }"
Note over Docs,IFrame: Theme sync established
Sidebar->>Phone: User selects persona + auth method
Sidebar->>Phone: User clicks Sign in action
Phone->>Phone: Show auth screen → promptGoogle/promptPasskey/promptEmail/promptOtp
Phone->>ApiPanel: pushCalls(signInCalls)
Phone->>Phone: "wallet.created = true"
loop Free actions
Sidebar->>Phone: User triggers add / send / withdraw
Phone->>Phone: AmountEntryScreen (interactive)
Phone->>ApiPanel: pushCalls(addMoneyCalls / sendCalls / withdrawCalls)
Phone->>Phone: Update balance + activity list
Sidebar->>Phone: User triggers card (runSimulated)
Phone->>Phone: "CardReveal animation → wallet.hasCard = true"
Sidebar->>Phone: User triggers tap (runSimulated)
Phone->>ApiPanel: pushCalls(transaction GET)
Phone->>Phone: "TapScreen → balance -= $7.32"
end
Sidebar->>Phone: Start over → reset()
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/components/Phone.tsx:149-154
**Unreachable `AmountScreen` branches in `render()`**
The `addmoney`, `send`, and `withdraw` cases in `render()` map to a non-interactive `AmountScreen` with hardcoded amounts, but these `ScreenId` values are never written to `transient` in `page.tsx`. `runAdd`/`runSend`/`runWithdraw` set `transient.screen = 'creating'`, and `phoneFromState()` only ever returns `'auth'`, `'wallet'`, or `'card'`. These three cases are dead code — if a future change accidentally routes here, visitors will see a static, non-interactive screen that ignores their entered amount.
### Issue 2 of 2
components/grid-wallet-demo/src/app/page.tsx:211-232
**Amount entry has no upper-bound guard, producing inconsistent API log and activity entries**
`runSend` (and similarly `runWithdraw`) accepts any positive dollar value from `AmountEntryScreen`. If a visitor adds $5,000 and then enters $10,000 to send, `pushCalls(sendCalls(Math.round(dollars * 1e6)))` logs a 10,000 USDB transfer while the wallet state clamps to `Math.max(0, 500000 - 1000000) = 0` — so the API panel says $10K but the activity row and balance both reflect $0 deducted. Adding a `Math.min(dollars, wallet.balanceCents / 100)` cap in the `AmountEntryScreen` call (or as a `max` prop on the input) would keep the API panel consistent with the balance story the demo tells.
Reviews (7): Last reviewed commit: "Add local Apple sign-in dev setup" | Re-trigger Greptile
| {hasFunds && ( | ||
| <div className={styles.changeRow}> | ||
| <span className={styles.deltaUp}>+ {phone.balance}</span> | ||
| <span className={styles.deltaChip}>Today</span> | ||
| </div> | ||
| )} |
There was a problem hiding this 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.
| {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.| typescript: { | ||
| // Origin is source-linked without its own node_modules, | ||
| // so its transitive type imports can't resolve from ../origin | ||
| ignoreBuildErrors: true, | ||
| }, |
There was a problem hiding this 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.
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!
- 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>
| 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; |
There was a problem hiding this 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.
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.
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.
How it's wired (same pattern as the Flow Builder)
components/grid-wallet-demo, embedded by the docs in an<iframe>with light/darkpostMessagetheme sync — identical to howgrid-visualizerpowers/flow-builder.@lightsparkdev/origintokens,sidebar (475px) + canvas + CodePanel-style API log,central-icons, squircle corners, theEmptyCanvasdotted background, theHeader/Footer.breadneobank 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-targetslocalhost:4000locally /grid-wallet-demo.vercel.appin prod.docs.json— nav entry under Overview (afterindex) + chrome-hide CSS for#wallet-demo-container.style.css— full-bleed iframe sizing (mirrors#flow-builder-container).Run locally
components/grid-wallet-demo(buildnpm run build, install--ignore-scriptsfor the central-icons license hook, same as grid-visualizer). The docs page is blank until this exists. Swap the URL indemo.mdxif different.What's faked (scripted, like the Flow Builder)
src/data/actions.ts.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