feat(grid-wallet-prod): fork the wallet demo into a production demo (phone + API panel, passkey-only) - #730
Conversation
Co-Authored-By: Claude Fable 5 <[email protected]>
…API panel Co-Authored-By: Claude Fable 5 <[email protected]>
…he layout Co-Authored-By: Claude Fable 5 <[email protected]>
…case Co-Authored-By: Claude Fable 5 <[email protected]>
… resize math Co-Authored-By: Claude Fable 5 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
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.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
pengying
marked this pull request as ready for review
July 23, 2026 00:00
Contributor
|
Too many files changed for review. ( |
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]>
…screen Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Forks
components/grid-wallet-demo→components/grid-wallet-prodper the spec in the parent PR:dev:appledropped, new README, HANDOFF.md removed. The demo and its docs embed are untouched.page.tsxrewritten to phone + resizable API panel only;ConfigurePaneldeleted; mobile configure⇄playground chrome (back pill, explore CTA, blur tray) removed from TSX and SCSS.useWalletDemoLogicpinspersona: 'fintech'andmethods: ['passkey'];setPersona/useCase/setUseCase/toggleMethodpruned. The phone's sign-in offers only Continue with Passkey.useColumnResizeno 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_CASESpruning,layout.tsxmetadata copy.Verification
npm run build+tscclean 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