Skip to content

feat(grid-wallet-prod): fork the wallet demo into a production demo (phone + API panel, passkey-only) - #730

Open
pengying wants to merge 13 commits into
07-21-docs_design_spec_for_grid-wallet-prod_fork_phase_1_remove_config_sidebar_from
07-21-feat_grid-wallet-prod_fork_grid-wallet-demo_as_the_production_demo_app
Open

feat(grid-wallet-prod): fork the wallet demo into a production demo (phone + API panel, passkey-only)#730
pengying wants to merge 13 commits into
07-21-docs_design_spec_for_grid-wallet-prod_fork_phase_1_remove_config_sidebar_from
07-21-feat_grid-wallet-prod_fork_grid-wallet-demo_as_the_production_demo_app

Conversation

@pengying

@pengying pengying commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Forks components/grid-wallet-democomponents/grid-wallet-prod per the spec in the parent PR:

  • Fork: byte-faithful copy; package renamed, dev/start on port 4001 (demo keeps 4000), dev:apple dropped, new README, HANDOFF.md removed. The demo and its docs embed are untouched.
  • Sidebar removed: page.tsx rewritten to phone + resizable API panel only; ConfigurePanel deleted; mobile configure⇄playground chrome (back pill, explore CTA, blur tray) removed from TSX and SCSS.
  • Hardcoded flow: useWalletDemoLogic pins persona: 'fintech' and methods: ['passkey']; setPersona/useCase/setUseCase/toggleMethod pruned. The phone's sign-in offers only Continue with Passkey.
  • Fix: useColumnResize no longer reserves the removed 400px configure column, so the API panel can use the full width (verified by live drag test: cap 880px → 1280px at a 1600px viewport).

Deferred to Phase 2 (live API integration spec): embed-path default-width math, orphaned UseCasePicker/USE_CASES pruning, layout.tsx metadata copy.

Verification

npm run build + tsc clean at every step; Playwright smoke test on :4001 confirmed no sidebar, passkey-only sign-in, and the API panel appending calls as the phone flow runs.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jul 25, 2026 1:33am
grid-wallet-demo Ignored Ignored Preview Jul 25, 2026 1:33am

Request Review

pengying commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@pengying pengying changed the title feat(grid-wallet-prod): fork grid-wallet-demo as the production demo app feat(grid-wallet-prod): fork the wallet demo into a production demo (phone + API panel, passkey-only) Jul 23, 2026
@pengying
pengying marked this pull request as ready for review July 23, 2026 00:00
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (613 files found, 100 file limit)

pengying and others added 6 commits July 22, 2026 17:10
Drop the "Save a passkey?" interstitial sheet — tapping Continue with
passkey now starts the WebAuthn create ceremony directly, which also keeps
the tap's user activation for the system dialog.

Co-Authored-By: Claude Fable 5 <[email protected]>
pengying and others added 2 commits July 24, 2026 18:31
…nd signed cash-out (Phase 2) (#740)

## Summary

Wires `components/grid-wallet-prod` from fully-scripted to the **real
Grid API** (public sandbox), per the Phase 2 spec
(`docs/superpowers/specs/2026-07-22-grid-wallet-prod-live-api-design.md`).
The API panel now renders the requests/responses that actually went over
the wire.

**The full money loop is live-verified end to end:** passkey sign-in →
Add $20 (real platform on-ramp, `COMPLETED`) → Cash out $5 (quote →
browser-signed `Grid-Wallet-Signature` execute → polled to `COMPLETED`
in ~62s), with the displayed balance tracking the real account
throughout.

### What's in here

- **Server proxy** (`/api/grid/[...path]`): allow-listed paths only,
Basic-auth injected server-side, `{request,response}` envelopes with
redacted auth — the panel logs real traffic.
- **Browser crypto** (`gridCrypto.ts`): TEK keygen, HPKE
encrypt/decrypt, Turnkey stamps via `@turnkey/*` — session keys never
leave the client; cross-checked against
`scripts/embedded-wallet-sign.js`.
- **Real auth** (`gridSession.ts`): first-run EMAIL_OTP bootstrap (magic
`000000` in sandbox) → registers a real Grid `PASSKEY` credential via
WebAuthn → returning sign-ins are pure passkey (challenge → `.get()` →
verify).
- **Webhook receiver** (`/api/webhooks`): `X-Grid-Signature` P-256
verification against `GRID_WEBHOOK_PUBKEY`, event ring buffer + poll
route.
- **Live money**: balance/activity reads, Add money via the real
platform on-ramp (the customer-wallet sandbox fund endpoint mints
unspendable book balance — discovered live), outbound
quote/stamp/execute/poll with completion checkmarks gated on
`COMPLETED`.
- 38 vitest tests across the pure modules; every task browser-verified
against the live sandbox.

### Env contract

`GRID_CLIENT_ID/SECRET`, `GRID_API_BASE_URL`, `GRID_CUSTOMER_ID`,
`GRID_WEBHOOK_PUBKEY` in `.env.local` (git-ignored). Prod cutover = swap
those values; first tap re-runs passkey registration against the
production customer.

### Tracked pre-prod follow-ups (not in this PR)

- ~~Gate `GET /api/webhooks/events`~~ — route removed; deliveries are
pushed over SSE instead of polled.
- Proxy customer-scoping before production keys (today it's a
single-customer sandbox gateway).
- ~~Wire real activity rows onto the phone~~ — done below.
- Phone-side failure states for execute errors (panel-only today).
- Report upstream: `POST /quotes` returns undocumented 200 for
platform-sourced quotes.

---

## Update — funding flows, webhook-driven arrivals, sandbox gating

A second pass over the same component. Everything below was verified
against the live sandbox in a browser; the API panel now shows **only**
real traffic.

### Auth follows the documented bootstrap order

- Sign-in leads with the credential a Global Account is actually born
with (`EMAIL_OTP`), with the entry step prefilled from the credential's
own `nickname` — no invented address.
- A passkey is added **later**, from the wallet, as its own signed
action: one device ceremony authorized by the live session (it used to
register *and* immediately authenticate, so a single tap asked for
approval twice).
- The CTA advertises the credential `signIn` will really use. Passkeys
are device-bound, so a passkey listed on the account but registered
elsewhere no longer sends this browser into an assertion it can't
satisfy.
- A lapsed session (15 min) can now re-authenticate in place: the OTP
sheet has a surface on the wallet screen, and the WebAuthn call runs
inside the tap's user activation instead of behind the re-auth
round-trip.

### Funding uses the endpoints Grid actually supports

- **Add money → country → deposit instructions**: the customer's own
account/routing/rails/reference, read from `GET
/customers/internal-accounts`. (The EUR block is a clearly-marked
placeholder IBAN in `data/placeholderDeposit.ts` until the customer has
an EUR account — the read already returns one section per fiat account,
so a real one appears with no code change.)
- **…or "add an account"** to pull from. A pull quote sources the
**ExternalAccount** — the previous body pointed at the platform's
internal account. Grid rejects `/execute` for such a quote (*"funds must
be pushed … use POST /sandbox/send"*), so the quote is created and left
pending, and `POST /sandbox/send` stands in for the push. Amounts here
are USD cents, not USDB micro-units.
- **Add from crypto**: Base / Solana / Ethereum, network → amount → the
real Grid-provisioned deposit address for that chain.
- Saved banks are seeded from `GET /customers/external-accounts`
(filtered to USD/EUR, ACTIVE); selecting one quotes against that account
instead of re-creating it — which also fixes the stale-recipient gap
noted in #740's original follow-ups.

### Webhooks

- **`X-Grid-Signature` parsing was rejecting every real delivery.** Grid
sends `{"v":"1","s":"…"}` (or a bare base64 header); we required
`{"version":1,"signature":"…"}`, so verification never ran and
everything 401'd. The earlier tests signed using our own assumed shape,
so they passed — they now assert the documented formats.
- Verified deliveries **stream to the panel over SSE**
(`/api/webhooks/stream`); nothing polls. The bus is pinned to
`globalThis` because Next gives each route handler its own module
instance — the publisher and subscriber were otherwise in different
registries.
- **An arrival, not a tap, is what says money landed**:
`INCOMING_PAYMENT.COMPLETED` drives the toast and Activity row from its
payload and re-reads the balance from the API. Confirming an add no
longer claims anything.

### Panel, wallet, packaging

- The panel shows only real requests: every synthesized call and
response body is gone (including a fabricated `POST
https://your-app.com/webhooks/grid` and ~330 lines of stubbed
quote/transaction/session responses). Flows with no client call behind
them log nothing.
- Activity renders the real `GET /transactions` history (EXPIRED quotes
filtered out); the balance hero shows **available** with the account
**total** beneath, since sandbox book balance runs ahead of spendable.
- Bank picker trimmed to the US + euro area (ACH + SEPA); APY 3%.
- `Dockerfile` + `.dockerignore` on `output: 'standalone'`.
**`NEXT_PUBLIC_GRID_SANDBOX` is a build arg** (NEXT_PUBLIC_* is inlined
at build time) and gates the sandbox-only "simulate funding" affordance
— a production image must be built without it.
- `scripts/ensure-business-customer.mjs`: idempotent `BUSINESS` customer
provisioning, writes `GRID_BUSINESS_CUSTOMER_ID` to `.env.local`.

60 vitest tests (was 38).

### Still open

- A received webhook only drives the *add* path;
`INCOMING_PAYMENT.PENDING` and `OUTGOING_PAYMENT.*` are logged but don't
touch the wallet.
- The sandbox funding stand-in for the *instructions* screen still runs
the platform on-ramp (no quote exists there to `/sandbox/send`).
- USDB-native funding over Spark works (`REALTIME_FUNDING` +
`cryptoNetwork: SPARK`, rate 1.0, verified) but isn't offered in the UI
— both current paths convert.
- `docker build` unverified: the daemon wasn't running here. The
standalone server itself was run and exercised (page, webhook, SSE,
proxy → Grid all responding).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <[email protected]>
….example

The README still described sign-in as "passkey only" and the live API as "Phase 2,
planned" — both contradicted by the code in this branch. It now documents what the
app actually does: EMAIL_OTP-first auth with a passkey added later, the push/pull
split for funding (and why a pull quote can't be executed), webhook-driven arrival
reporting over SSE with the ngrok setup, the sandbox flag and Docker build, and the
sandbox quirks worth knowing (skipped email delivery, book-vs-spendable balance).

.env.example documents the env contract the README refers to; .env.local stays
git-ignored and out of the repo.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Pjqydq34z8DBK1kBrioXK2
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.

1 participant