Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .sync-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"name": "privy-next-starter",
"path": "privy-next-starter",
"targets": [
"examples/privy-next-cards",
"examples/privy-next-cross-app-connect",
"examples/privy-next-cross-app-provider",
"examples/privy-next-farcaster",
Expand Down Expand Up @@ -89,6 +90,20 @@
"examples/privy-next-solana": {
"exclude": ["src/components/sections/wallet-actions.tsx"],
"reason": "Uses solana-specific wallet actions"
},
"examples/privy-next-cards": {
"exclude": [
"src/components/sections/wallet-actions.tsx",
"src/components/sections/create-a-wallet.tsx",
"src/components/sections/fund-wallet.tsx",
"src/components/sections/link-accounts.tsx",
"src/components/sections/unlink-accounts.tsx",
"src/components/sections/signers.tsx",
"src/components/sections/wallet-management.tsx",
"src/components/sections/mfa.tsx",
"src/components/sections/user-object.tsx"
],
"reason": "Cards demo shows only the card signup and summary flow"
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions examples/privy-next-cards/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NEXT_PUBLIC_PRIVY_APP_ID=your-privy-app-id

# Optional. Only needed for the "Simulate a purchase" button, which calls Stripe Issuing's test
# helpers from a route handler. Must be a TEST-mode key (sk_test_… or rk_test_…) for the same Stripe
# account that issues the cards. Server-only — never prefix this with NEXT_PUBLIC_.
STRIPE_SECRET_KEY=
42 changes: 42 additions & 0 deletions examples/privy-next-cards/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*
!.env.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
21 changes: 21 additions & 0 deletions examples/privy-next-cards/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Privy

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
137 changes: 137 additions & 0 deletions examples/privy-next-cards/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Cards with Privy

This Next.js example shows how to issue and manage cards for your users with the Privy React SDK, using the `SignUpForCardView` and `CardSummaryView` components from `@privy-io/react-auth/ui`.

It is scaffolded from [`privy-next-starter`](../../privy-next-starter), trimmed down to just the card flow — the wallet, funding, linking, signer, and MFA sections are intentionally removed (opted out under `sectionOverrides` in [`.sync-manifest.json`](../../.sync-manifest.json), so base syncs don't re-add them).

> **⚠️ A card cannot spend in production yet.** `SignUpForCardView` grants the card's USDC spend allowance to a Bridge spender address, and the SDK only publishes those addresses for sandbox testnets. In production it skips the approval and still reports the card as ready, so the card exists with no allowance behind it. Both environments are available in the demo's toggle — see [Environments](#environments).

## Flow

1. Sign in with Privy. The provider creates an embedded Ethereum wallet for users without one.
2. Press **Sign up for a card**. `SignUpForCardView` walks the e-sign disclosure, the bank agreements, Bridge terms, and KYC, then creates the card and prompts for the on-chain USDC spend approval.
3. On success it fires `onCardReady` with the card id, and the demo hands straight off to the summary.
4. `CardSummaryView` shows the balance, card face, transactions, card details and reveal, and statement downloads.
5. Optionally press **Simulate a $0.50 purchase** to put a real row on the transaction list. See [Simulating a purchase](#simulating-a-purchase).

A pill next to the funding wallet address shows whether a card exists yet. The demo finds an existing card by listing the user's cards for the selected environment, so a reload goes straight to the summary. A returning user who runs signup again still starts at "Get started", but the SDK reuses the existing card rather than creating a second one (one card per account).

## Source map

- [`src/app/page.tsx`](./src/app/page.tsx): Login state, page layout, and the `Cards` section
- [`src/providers/providers.tsx`](./src/providers/providers.tsx): Privy provider configuration; EVM-only, creates an embedded Ethereum wallet on login
- [`src/components/sections/cards.tsx`](./src/components/sections/cards.tsx): Hosts both card views, owns the environment toggle and card id, and holds the chain map and developer name
- [`src/components/sections/cards-api.ts`](./src/components/sections/cards-api.ts): Lists the user's cards for an environment, since the SDK exports no card-list hook
- [`src/components/sections/find-embedded-wallet.ts`](./src/components/sections/find-embedded-wallet.ts): Picks the embedded EVM wallet whose Privy wallet id the card views need
- [`src/components/sections/modal.tsx`](./src/components/sections/modal.tsx): Centered 440px modal the card views render inside
- [`src/components/sections/simulate-spend.ts`](./src/components/sections/simulate-spend.ts): Resolves the Privy card id to its Stripe card id, then calls the test-spend route
- [`src/app/api/test-spend/route.ts`](./src/app/api/test-spend/route.ts): Server-only route that creates and captures a Stripe Issuing test authorization

## Quick start

### 1. Clone the example

```bash
mkdir -p privy-next-cards && curl -L https://github.com/privy-io/privy-examples/archive/main.tar.gz | tar -xz --strip=3 -C privy-next-cards examples-main/examples/privy-next-cards && cd privy-next-cards
```

### 2. Install dependencies

```bash
pnpm install
```

The card views ship in a prerelease, so `@privy-io/react-auth` is pinned to an exact beta rather than a caret range. Move it to the stable release once one contains both components.

### 3. Configure environment

Copy the example environment file:

```bash
cp .env.example .env.local
```

Set the public Privy app ID:

```env
NEXT_PUBLIC_PRIVY_APP_ID=your_app_id_here
```

Variables prefixed with `NEXT_PUBLIC_` are exposed to the browser. Do not add secrets to client components. If a card flow needs an app secret, call it from a route handler under `src/app/api/` and read the secret from a non-public env var.

### 4. Configure the Privy dashboard

In the [Privy dashboard](https://dashboard.privy.io), configure the app used by `NEXT_PUBLIC_PRIVY_APP_ID`:

- Enable login methods for the users who will try the demo.
- Enable embedded wallets.
- Enable cards for the app.
- Save the app's Stripe publishable key in the cards config. `CardSummaryView` fetches it itself from `GET /api/v1/apps/:app_id/cards/config` — there is no key prop. If it is missing, **Show details** stays silently inert.
- Make sure the Bridge **sandbox** account behind the app's card configuration has the `cards` endorsement. Sandbox and production are separate Bridge accounts with separate capabilities, so enabling cards in production does not cover sandbox. Without it, signup fails with `'cards' endorsement not allowed — Cards is not enabled on the developer account`, which no amount of retrying or KYC will clear.

### 5. Fund the wallet

The card spends USDC on Base Sepolia from the user's embedded wallet, and the approval transaction needs gas. The demo shows the wallet address and links to both faucets once you log in. Without Base Sepolia ETH the flow dead-ends on the approval step with generic error copy.

### 6. Start the development server

```bash
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) in a browser.

## Environments

A **sandbox / production** toggle sits at the top of the Cards section. It switches the `environment` prop on both card views, the funding chain, and which ledger the card lookup queries. It defaults to sandbox on every load rather than persisting, so you can't land in production by accident.

Production needs its own credentials, separate from sandbox: a Bridge production integration with the `cards` endorsement on that live account, and a live Stripe key in the app's production card configuration. Sandbox credentials do not carry over.

| | Sandbox | Production |
| --- | --- | --- |
| Funding chain | Base Sepolia (`eip155:84532`) | Base (`eip155:8453`), real USDC |
| Card signup | ✅ | ✅ |
| Card summary | ✅ | ✅ |
| Simulated purchase | ✅ | ❌ not possible |

**A production card cannot spend yet.** `SignUpForCardView` grants the card's USDC allowance to a Bridge spender, and the SDK publishes those addresses for sandbox testnets only. In production it skips the approval and still reports the card `ready`, so the card exists but has no allowance behind it. Production signup is still enabled so you can exercise the real onboarding path — live Bridge customer, real KYC, real Stripe card — and the demo says so on screen. Spend starts working once mainnet spenders ship, with no change needed here.

**Why simulated purchases are sandbox-only.** Stripe's Issuing test helpers do not exist for live keys — there is no API to fabricate a live authorization, so live spend must be a real purchase at a real merchant. The button is hidden in production, and the route refuses non-test keys.

Production mode needs its own credentials, separate from sandbox: a Bridge **production** integration with the `cards` endorsement on that live account, and a **live** Stripe key in the app's production card configuration. Sandbox credentials do not carry over.

## Simulating a purchase

`CardSummaryView`'s transaction list is fed from Stripe Issuing, so a new card has nothing to show. **Simulate a $0.50 purchase** creates an authorization with [Stripe's Issuing test helpers](https://docs.stripe.com/api/issuing/authorizations/test-helpers-create) and captures it, which puts a real row on the list.

To enable it, set a **test-mode** Stripe secret key for the account that issues the cards:

```env
STRIPE_SECRET_KEY=sk_test_…
```

Notes:

- This is a **server-only** variable, read by [`src/app/api/test-spend/route.ts`](./src/app/api/test-spend/route.ts). Never expose a Stripe secret key to the browser via `NEXT_PUBLIC_`. The route refuses to run with anything that isn't an `sk_test_`/`rk_test_` key and caps the amount at $100.
- The button is a demo affordance, not something to deploy. Anyone who can reach the route can create authorizations on the configured account's cards.
- **It does not move the balance.** `CardSummaryView` shows the funding wallet's on-chain balance, so a simulated purchase changes the transaction list only. Real settlement pulls USDC through the Bridge spender.
- Reopen the modal to see the new row — closing it unmounts the view, so opening again refetches.
- Stripe may **decline** the authorization, since stablecoin-backed cards check funding. A declined authorization still appears in the list, and the toast reports the reason.

## SDK version

`CardSummaryView` and `SignUpForCardView` are not in a stable `@privy-io/react-auth` release yet, so this example pins the beta that contains both.

To check what you actually have installed:

```bash
rg 'SignUpForCardView|CardSummaryView' node_modules/@privy-io/react-auth/dist/dts/ui.d.ts
```

Both names must appear. If you swap in a locally built tarball to test unreleased SDK changes, note that a local build and a published release can share a version string with different contents, so a later `pnpm install` can silently replace it with no resolution error — run the check above again after any install. Don't commit a `file:` dependency; it is machine-local.

## Relevant links

- [Privy Dashboard](https://dashboard.privy.io)
- [Privy Documentation](https://docs.privy.io)
- [React SDK](https://www.npmjs.com/package/@privy-io/react-auth)
30 changes: 30 additions & 0 deletions examples/privy-next-cards/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
"node_modules/**",
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
],
},
{
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
];

export default eslintConfig;
7 changes: 7 additions & 0 deletions examples/privy-next-cards/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
};

export default nextConfig;
31 changes: 31 additions & 0 deletions examples/privy-next-cards/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "privy-next-cards",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@privy-io/react-auth": "3.38.0-beta-20260821205628",
"next": "15.5.7",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.5.7",
"tailwindcss": "^4",
"typescript": "^5"
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
}
Loading