feat(privy-next-cards): cards demo — sign up, manage, and test-spend - #169
Draft
madeleine-c wants to merge 13 commits into
Draft
feat(privy-next-cards): cards demo — sign up, manage, and test-spend#169madeleine-c wants to merge 13 commits into
madeleine-c wants to merge 13 commits into
Conversation
Adds `examples/privy-next-cards` as the scaffold for the Cards demo, created with `pnpm create-example cards --base=next` so the example is registered in `.sync-manifest.json` and picks up future base updates. The demo is trimmed to just the card flow: sign in with Privy, sign up for a card, then review the card summary. The wallet, funding, linking, signer, MFA, and wallet-action sections from `privy-next-starter` are removed and opted out under `sectionOverrides` so base syncs do not re-add them. `UserObject` is kept as the debug panel. `src/components/sections/cards.tsx` drives the flow with local state and renders a placeholder for the signup and summary steps. The real card components are added on top of this later; the README documents where they go. Also drops viem, bs58, and the @solana-program packages, which no longer have importers after the section removals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
madeleine-c
marked this pull request as draft
August 21, 2026 15:20
Replaces the placeholder card section with the real components from `@privy-io/react-auth/ui`, so the demo can be tested end to end: - `SignUpForCardView` runs the pre-card journey (e-sign disclosure, bank agreements, Bridge terms, KYC, card creation, USDC spend approval) and hands back a card id via `onCardReady`. - `CardSummaryView` then shows balance, transactions, card details and reveal, and statement downloads. Both render inside a new 440px right-anchored drawer (`src/components/ui/side-panel.tsx`), matching the width the components are designed against. Written with plain Tailwind plus a keydown listener rather than pulling in `@headlessui/react`. `SignUpForCardView` needs a Privy wallet id, which `useWallets()` does not expose, so `get-embedded-wallets.ts` picks the embedded Ethereum wallet out of `user.linkedAccounts` and narrows its optional `id` to a string. Environment (`sandbox`), chain (`eip155:84532`), and the disclosure's developer name are constants at the top of `cards.tsx`. The card id is persisted in `localStorage` per user so a reload reopens the summary rather than re-walking signup. Signup would reuse the same card regardless, since there is one card per account. Providers drop the Solana config and the demo drops `@solana/kit`: the card funds from an EVM chain and the wallet must be an `ethereum` one, so Solana only made wallet selection ambiguous. Also adds a funding block with the wallet address and faucet links, since the approval step needs Base Sepolia gas and the card needs testnet USDC. Requires a react-auth release containing both components; the dependency is provisionally set to ^3.38.0 and `pnpm-lock.yaml` still needs regenerating once that release exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Quality pass over the demo now that the flow is complete. No behavior change except the bundle split. Sync-safety: - Move `side-panel.tsx` out of `src/components/ui/`, which is an `always`-synced glob in `.sync-manifest.json`. It was the only example-owned file under `ui/` in the repo, so a base starter that ever added its own `side-panel.tsx` would have silently overwritten it with no conflict detection. `sections/` is an explicit filename list, which cannot collide, and is where sibling examples keep bespoke components. Bundle: - Load both card views with `next/dynamic` (`ssr: false`). Statically importing them put the views and their transitive deps in the initial `/page` chunk list, which every visitor paid for at first paint even though neither view is reachable without a click. Initial `/page` JS drops from 3532kB across 30 files to 3094kB across 24, and no card-view chunk remains in the initial list. They only render inside a client-only drawer, so nothing was server-rendered. Simplification: - `SidePanel` loses its `open` prop: mounted *is* open, so the prop restated conditional rendering and the same fact appeared in both the effect guard and an early return. Callers now mount it conditionally, which also folds the inner `wallet ?` / `cardId ?` guards into the same condition and drops two `: null` arms. - Drop the `previousOverflow` capture. Nothing else writes `body.style.overflow` and only one panel can be open, so the saved value was always `""`. The lock itself stays — the page only hides overflow at `md` and up. - `getEmbeddedWallets` -> `findEmbeddedWallet`: it returned a list whose `[0]` was taken immediately at its one call site. Also unexports the type alias and drops a redundant length check. - Collapse the card-id storage to a single derived `storageKey`, removing a module-level key helper, two repeated `user?.id` guards, and an unreachable reset branch (the section unmounts on logout). - Drop `useMemo` on the wallet lookup and `useCallback` on `onCardReady`; neither consumer is memoized, and the file was teaching two conventions at once. `closeDrawer` keeps its `useCallback` — it is in the panel effect's deps, so without it every render would tear down the Escape listener and rewrite `body.style.overflow`. - Remove a leftover wrapper `<div>` in `page.tsx` that grouped eight sections in the base starter and now wraps one child with no classes. - Also narrows the `onCardReady` closure to capture a user id string rather than the whole user object. Dependencies: - `@privy-io/react-auth` was `^3.38.0`, which 404s — nothing that high is published, so README step 2 (`pnpm install`) hard-failed for every reader before they reached the local-build instructions. Pin the latest published `^3.37.4` and let the README carry the unreleased-SDK story. - Regenerate `pnpm-lock.yaml`, which still recorded the base starter's `^3.12.0` specifier plus `@solana/kit` and `@solana-program/*` importers this example no longer declares. Any `--frozen-lockfile` install would have failed on the mismatch. Docs: - Trim the README: drop the Configuration table that restated the constants' own doc comments in less precise words, fold the sync-manifest note and the unreleased-SDK teaser into existing prose, and stop restating the in-app faucet copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Drops the commented-out `NEXT_PUBLIC_PRIVY_SIGNER_ID` and `NEXT_PUBLIC_SOLANA_MAINNET_RPC_URL` hints inherited from the base starter. This example has no signers section, and the Solana provider config was removed, so neither variable is read anywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Swaps the right-anchored drawer for a centered modal. Same 440px width the card views are designed against, now with rounded corners, capped at 90vh with its own scroll area so tall content (the transaction list, the KYC steps) still fits on short viewports. Escape, backdrop click, and the body scroll lock are unchanged, as is the conditional mounting — being mounted is still being open, so neither card view fetches until its modal is on screen. Renames `SidePanel` to `Modal` and the `Drawer` state union to `OpenView`, so nothing in the file still reads as a drawer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
$12.40 was needlessly large for a demo that just needs a row on the transaction list, and it eats more of a sandbox card's funding per click. Derives the button label from the amount rather than repeating it, so the copy cannot drift from what is actually charged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
- Adds the two test-spend files to the source map, which had gone stale.
- Adds the simulated purchase as a step in the flow, and mentions the
card-exists pill.
- Documents the Bridge sandbox `cards` endorsement as a dashboard
prerequisite. Sandbox and production are separate Bridge accounts with
separate capabilities, so enabling cards in production does not cover
sandbox, and the resulting failure ("'cards' endorsement not allowed")
is not something retrying or KYC can clear. This blocked the demo in
practice and was the least obvious setup step.
Also reverts the customized page metadata in `src/app/layout.tsx`.
`layout.tsx` is an `always`-synced file, so the custom title showed up as
critical drift and `pnpm sync:apply` would have silently reverted it.
Every sibling example keeps the base title. `pnpm check-drift` is now
clean for this example.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
"Sign up for a card" stayed available after a card was created, which read as an offer to create a second one. There is one card per account, so the flow would have walked the disclosure steps and handed back the same card — misleading rather than harmful, but no reason to show it. Also swaps the funding-wallet copy from "Before signing up, give it" to "Keep it topped up with" once a card exists, since the pre-signup framing was stale at that point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Removes the `UserObject` debug panel, leaving the card flow as the only thing on the page. Deletes the section, opts it out under `sectionOverrides` so base syncs do not re-add it, and drops the now-pointless row split in `page.tsx` — `md:flex-row` and `flex-grow` only existed to place the panel beside the main column. Verified `pnpm sync --target=privy-next-cards` still copies 0 files and `pnpm check-drift` is clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Switches the `environment` prop on both card views, the funding chain (Base Sepolia vs Base mainnet), and which ledger the card lookup queries. Defaults to sandbox on every load rather than persisting, so the demo cannot be left sitting in production. Production is read-only. `SignUpForCardView` grants the card's USDC allowance to a Bridge spender, and the shipped beta only contains sandbox testnet spenders — grepping its bundle for `eip155:` returns testnets only. In production the approval is skipped and the card is still reported `ready`, so signup would hand back a card that cannot spend with nothing in the UI to say so. Signup is therefore hidden in production behind `ALLOW_PRODUCTION_SIGNUP`, with an on-screen explanation, and the flag can be flipped once mainnet spenders ship. Simulated purchases are hidden in production too: Stripe's Issuing test helpers do not exist for live keys, so a live authorization cannot be fabricated at all. Live spend has to be a real purchase. Replaces the `localStorage` card-id cache with a real lookup (`cards-api.ts`, extracted from the test-spend path). The cache could not serve the production side — signup is disabled there, so the card has to be discovered — and it also made the pill lie when site data was cleared. The lookup is scoped per environment and ignores stale responses if the toggle moves mid-flight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
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
Adds
examples/privy-next-cards, a demo of the card issuing flow, wired to the real components from@privy-io/react-auth/ui.Sign in with Privy → sign up for a card → card summary → simulate a purchase.
SignUpForCardViewin a modal — e-sign disclosure, bank agreements, Bridge terms, KYC, card creation, then the on-chain USDC spend approval.onCardReadyhands the card id straight toCardSummaryView— balance, card face, transactions, card details + reveal, statement downloads.Verified end to end against a sandbox app: signup completes, the summary renders, and simulated purchases land on the transaction list.
Before merging
3.38.0-beta-20260821205628, the first build containing both components. Pinned exactly, not with a caret, because a caret on a prerelease also matches later prereleases. Move it to the stable release once one ships, and regenerate the lockfile./api/test-spendshould not be deployed as-is. Anyone who can reach it can create authorizations on whatever Stripe account the key points at. It's gated to test-mode keys and capped at $100, but it's a local demo affordance, not a public endpoint. Remove or gate it before deploying anywhere.Notable choices
SignUpForCardViewonly has published Bridge spender addresses for sandbox testnets; withproductionit skips the spend approval and still reports the card ready, leaving a card that can't spend. Chain iseip155:84532(Base Sepolia). Both are constants at the top ofcards.tsx, andas constmeans a stray"production"is a type error.walletIdneeds a helper.SignUpForCardViewtakes a Privy wallet id, butuseWallets()exposes only addresses, sofind-embedded-wallet.tspicks the embedded EVM wallet out ofuser.linkedAccounts.WalletWithMetadata.idis optional, so it narrows to a required string rather than casting at the call site.next/dynamic. Static imports put them and their transitive deps in the initial/pagechunk list, which every visitor paid for at first paint even though neither view is reachable without a click. Measured: 3532kB/30 files → 3094kB/24 files, with no card-view chunk in the initial list.@headlessui/react. Mounted conditionally, so neither view fetches until it's on screen.STRIPE_SECRET_KEY— the case the Next base was chosen for over the Vite starter. It resolves the Privy card id to the Stripe card id viaprovider_idonGET /api/v1/cards, using a raw authenticated request because the SDK exports no card-list hook.@solana/kitdropped. The card funds from an EVM chain and the wallet must beethereum, so Solana only made wallet selection ambiguous.wallet-actions,create-a-wallet,fund-wallet,link-accounts,unlink-accounts,signers,wallet-management,mfa) are opted out undersectionOverridesin.sync-manifest.json.pnpm sync --target=privy-next-cardscopies 0 files andpnpm check-driftis clean.Setup notes worth reading
Two prerequisites cost real debugging time and are now documented in the README:
cardsendorsement. Sandbox and production are separate Bridge accounts with separate capabilities, so enabling cards in production doesn't cover sandbox. Without it signup fails with'cards' endorsement not allowed, and neither retrying nor KYC clears it.CardSummaryViewfetches it itself and has no key prop, so if it's missing Show details is silently inert rather than erroring.Testing
tsc --noEmit,pnpm lint,pnpm buildclean.git clonethe branch →pnpm install --frozen-lockfile→pnpm buildall succeed. Note the build fails with the placeholder app id (Cannot initialize the Privy provider with an invalid Privy app ID), so a realNEXT_PUBLIC_PRIVY_APP_IDin.env.localis required — same as every other Privy example.SDK feedback
Papercuts found while integrating, worth fixing in the SDK rather than in every consumer:
walletIdisn't reachable fromuseWallets(), andWalletWithMetadata.idis optional, so consumers hand-roll a filter and a type narrowing.internal-demohand-rolls the same filter.SANDBOX_SPEND_APPROVAL_CHAINSknows which sandbox chains work, but consumers must hardcode CAIP-2 ids and separately know which are supported —internal-democopies a 13-entry table.environment: 'production'fails silently: it skips the spend approval and still reportsready. A dev-mode warning would beat a doc comment.GET /api/v1/cardswith a raw token fetch — which is exactly what this demo does for test spend.pnpm installcan silently swap in a build missingSignUpForCardViewwith no resolution error.CardSummaryViewrequiresdeveloperNamewhileSignUpForCardViewdroppedappName, leaving no way to brand the signup screen.Why next rather than react
Both starters pin the same SDK, but 16 of 18 existing examples are
privy-next-*(the two react ones are react for platform reasons),CONTRIBUTING.mdis written around next paths, and Next route handlers are what make the server-side test-spend call possible.🤖 Generated with Claude Code