@dodopayments/opencode-plugin 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/opencode-plugin/README.md +1 -1
- package/package.json +1 -1
- package/skills/better-auth-integration/SKILL.md +12 -3
- package/skills/billing-sdk/SKILL.md +15 -167
- package/skills/billing-sdk/references/pricing-page.md +197 -0
- package/skills/checkout-integration/SKILL.md +31 -460
- package/skills/checkout-integration/references/overlay-checkout.md +148 -0
- package/skills/checkout-integration/references/server-examples.md +317 -0
- package/skills/credit-based-billing/SKILL.md +11 -158
- package/skills/credit-based-billing/references/ai-token-example.md +108 -0
- package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
- package/skills/customer-management/SKILL.md +25 -17
- package/skills/discounts-and-promotions/SKILL.md +20 -17
- package/skills/dodo-best-practices/SKILL.md +18 -5
- package/skills/dodo-mcp-usage/SKILL.md +152 -0
- package/skills/framework-adapters/SKILL.md +64 -421
- package/skills/framework-adapters/references/astro.md +52 -0
- package/skills/framework-adapters/references/bun.md +45 -0
- package/skills/framework-adapters/references/convex.md +91 -0
- package/skills/framework-adapters/references/express.md +72 -0
- package/skills/framework-adapters/references/fastify.md +54 -0
- package/skills/framework-adapters/references/hono.md +63 -0
- package/skills/framework-adapters/references/nuxt.md +53 -0
- package/skills/framework-adapters/references/remix.md +65 -0
- package/skills/framework-adapters/references/sveltekit.md +49 -0
- package/skills/framework-adapters/references/tanstack-start.md +31 -0
- package/skills/license-keys/SKILL.md +117 -367
- package/skills/license-keys/references/cli-tool.md +143 -0
- package/skills/license-keys/references/desktop-app.md +170 -0
- package/skills/localized-pricing/SKILL.md +51 -29
- package/skills/mobile-checkout/SKILL.md +72 -310
- package/skills/mobile-checkout/references/android.md +69 -0
- package/skills/mobile-checkout/references/flutter.md +97 -0
- package/skills/mobile-checkout/references/ios.md +66 -0
- package/skills/mobile-checkout/references/react-native.md +100 -0
- package/skills/product-catalog-management/SKILL.md +39 -16
- package/skills/refunds-and-disputes/SKILL.md +91 -71
- package/skills/subscription-integration/SKILL.md +43 -64
- package/skills/subscription-integration/references/webhook-events.md +82 -0
- package/skills/testing-and-go-live/SKILL.md +22 -15
- package/skills/usage-based-billing/SKILL.md +16 -122
- package/skills/usage-based-billing/references/instrumentation.md +119 -0
- package/skills/webhook-integration/SKILL.md +31 -263
- package/skills/webhook-integration/references/common-mistakes.md +146 -0
- package/skills/webhook-integration/references/event-catalog.md +103 -0
- package/skills/webhook-integration/references/local-testing.md +42 -0
package/README.md
CHANGED
|
@@ -175,6 +175,7 @@ If you prefer the local stdio API server with your own API key instead of the de
|
|
|
175
175
|
| `dodo-best-practices` | SDK setup, environments, API keys, and the canonical checkout-to-webhook architecture |
|
|
176
176
|
| `framework-adapters` | Official `@dodopayments/*` handlers for Next.js, Express, Hono, Astro, Remix, SvelteKit, Nuxt, Fastify, TanStack, Bun, Convex |
|
|
177
177
|
| `testing-and-go-live` | Test mode, test payment methods, webhook testing, production launch checklist |
|
|
178
|
+
| `dodo-mcp-usage` | When to use the documentation vs API MCP servers, test-mode safety rules, auth, troubleshooting |
|
|
178
179
|
|
|
179
180
|
**Accepting payments**
|
|
180
181
|
|
|
@@ -37,7 +37,7 @@ opencode run "List every skill available to you by name."
|
|
|
37
37
|
|
|
38
38
|
## What you get
|
|
39
39
|
|
|
40
|
-
Agent skills (auto-loaded when relevant): `dodo-best-practices`, `framework-adapters`, `testing-and-go-live`, `checkout-integration`, `subscription-integration`, `mobile-checkout`, `webhook-integration`, `credit-based-billing`, `usage-based-billing`, `license-keys`, `product-catalog-management`, `discounts-and-promotions`, `localized-pricing`, `customer-management`, `refunds-and-disputes`, `billing-sdk`, `better-auth-integration`.
|
|
40
|
+
Agent skills (auto-loaded when relevant): `dodo-best-practices`, `dodo-mcp-usage`, `framework-adapters`, `testing-and-go-live`, `checkout-integration`, `subscription-integration`, `mobile-checkout`, `webhook-integration`, `credit-based-billing`, `usage-based-billing`, `license-keys`, `product-catalog-management`, `discounts-and-promotions`, `localized-pricing`, `customer-management`, `refunds-and-disputes`, `billing-sdk`, `better-auth-integration`.
|
|
41
41
|
|
|
42
42
|
Two MCP servers (registered automatically):
|
|
43
43
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: better-auth-integration
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments plugin for Better Auth (@dodopayments/better-auth) covering customer sync on signup, authenticated checkout, customer portal, usage ingestion, and verified webhooks. Use when an app uses Better Auth and needs the dodopayments, dodopaymentsClient, checkout, portal, usage, or webhooks plugins; for other auth stacks use framework-adapters.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Better Auth Integration
|
|
@@ -18,9 +18,11 @@ Use `@dodopayments/better-auth` to synchronize Better Auth users with Dodo Payme
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install @dodopayments/better-auth dodopayments better-auth
|
|
21
|
+
npm install @dodopayments/better-auth dodopayments better-auth zod
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Requires Better Auth 1.4 or a later 1.x release.
|
|
25
|
+
|
|
24
26
|
The package exports the lowercase server functions `dodopayments`, `checkout`, `portal`, `usage`, and `webhooks`. Its client entry point exports `dodopaymentsClient`.
|
|
25
27
|
|
|
26
28
|
## Server setup
|
|
@@ -76,6 +78,13 @@ export const auth = betterAuth({
|
|
|
76
78
|
|
|
77
79
|
`products` maps an application slug to a Dodo product ID. `authenticatedUsersOnly: true` requires a Better Auth session for checkout. `getCustomerParams` adds application metadata when customer synchronization runs.
|
|
78
80
|
|
|
81
|
+
The plugin adds a `dodoCustomerId` field to the Better Auth `user` table. After adding the plugin, update your database schema with the [Better Auth CLI](https://www.better-auth.com/docs/concepts/cli):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx auth generate # generate the ORM schema or SQL migration, then apply it yourself
|
|
85
|
+
npx auth migrate # or apply it directly (built-in Kysely adapter only)
|
|
86
|
+
```
|
|
87
|
+
|
|
79
88
|
## Browser client setup
|
|
80
89
|
|
|
81
90
|
```typescript
|
|
@@ -170,7 +179,7 @@ if (error) {
|
|
|
170
179
|
}
|
|
171
180
|
```
|
|
172
181
|
|
|
173
|
-
The plugin derives `customer_id` from the authenticated session. `event_id` is the event's idempotency key; reuse it when retrying the same event.
|
|
182
|
+
Both `usage.ingest` and `usage.meters.list` require a signed-in user with a verified email address. The plugin derives `customer_id` from the authenticated session. `event_id` is the event's idempotency key; reuse it when retrying the same event.
|
|
174
183
|
|
|
175
184
|
## Webhook endpoint
|
|
176
185
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: billing-sdk
|
|
3
|
-
description:
|
|
3
|
+
description: BillingSDK, the open-source React and shadcn component library for Dodo Payments billing UI. Use when building pricing tables, pricing pages, account billing pages, usage meters, credit balances, invoice history, or cancel and upgrade UI in React or Next.js, or running @billingsdk/cli init; keep Dodo API calls server-side.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# BillingSDK
|
|
@@ -75,8 +75,11 @@ From the current official registry, grouped by purpose:
|
|
|
75
75
|
| Payments | `payment-details`, `payment-details-two`, `payment-method-selector`, `payment-card`, `payment-failure` |
|
|
76
76
|
| Promotion and trials | `banner`, `limited-offer-dialog`, `trial-expiry-card` |
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
Most blocks have a published usage example and props table on their page at
|
|
79
|
+
[billingsdk.com/docs/components](https://billingsdk.com/docs/components) (for example `subscription-management`
|
|
80
|
+
and `billing-settings`); `pricing-table-one` is reproduced in
|
|
81
|
+
[references/pricing-page.md](references/pricing-page.md). The generated file's props type is still
|
|
82
|
+
authoritative for the version you installed, so read it after running `add`.
|
|
80
83
|
|
|
81
84
|
## Server client
|
|
82
85
|
|
|
@@ -101,169 +104,14 @@ are `https://test.dodopayments.com` and `https://live.dodopayments.com`.
|
|
|
101
104
|
|
|
102
105
|
## Pricing page
|
|
103
106
|
|
|
104
|
-
|
|
107
|
+
Full guide: [references/pricing-page.md](references/pricing-page.md).
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
the component — the amount actually charged comes from the Dodo product catalogue, and Dodo API amounts
|
|
108
|
-
are always in the smallest currency unit (cents). Treat this file as a mirror of the dashboard and
|
|
109
|
-
reconcile it when you change a price.
|
|
109
|
+
Covers:
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
export interface Plan {
|
|
116
|
-
id: PlanId;
|
|
117
|
-
title: string;
|
|
118
|
-
price: number;
|
|
119
|
-
period: string;
|
|
120
|
-
features: string[];
|
|
121
|
-
popular: boolean;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export const PLANS: readonly Plan[] = [
|
|
125
|
-
{
|
|
126
|
-
id: 'starter',
|
|
127
|
-
title: 'Starter',
|
|
128
|
-
price: 9,
|
|
129
|
-
period: 'month',
|
|
130
|
-
features: ['100 requests', 'Basic support', '1 project'],
|
|
131
|
-
popular: false,
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
id: 'pro',
|
|
135
|
-
title: 'Pro',
|
|
136
|
-
price: 29,
|
|
137
|
-
period: 'month',
|
|
138
|
-
features: ['Unlimited requests', 'Priority support', '10 projects'],
|
|
139
|
-
popular: true,
|
|
140
|
-
},
|
|
141
|
-
];
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### 2. Product id map — server only
|
|
145
|
-
|
|
146
|
-
Never let the browser choose an arbitrary `product_id`. Map the public plan slug to a `pdt_` id on the
|
|
147
|
-
server and reject anything else.
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
// lib/plan-products.server.ts
|
|
151
|
-
import 'server-only';
|
|
152
|
-
import type { PlanId } from './plans';
|
|
153
|
-
|
|
154
|
-
const PRODUCT_IDS: Record<PlanId, string> = {
|
|
155
|
-
starter: 'pdt_starter_monthly',
|
|
156
|
-
pro: 'pdt_pro_monthly',
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
export function productIdForPlan(value: unknown): string | null {
|
|
160
|
-
if (typeof value !== 'string') return null;
|
|
161
|
-
return PRODUCT_IDS[value as PlanId] ?? null;
|
|
162
|
-
}
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### 3. Checkout route
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
// app/api/checkout/route.ts
|
|
169
|
-
import { NextResponse } from 'next/server';
|
|
170
|
-
import { dodo } from '@/lib/dodopayments';
|
|
171
|
-
import { productIdForPlan } from '@/lib/plan-products.server';
|
|
172
|
-
import { getCurrentUser } from '@/lib/auth';
|
|
173
|
-
|
|
174
|
-
export async function POST(request: Request) {
|
|
175
|
-
const user = await getCurrentUser();
|
|
176
|
-
if (!user) {
|
|
177
|
-
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const body: unknown = await request.json();
|
|
181
|
-
const planId = (body as { planId?: unknown }).planId;
|
|
182
|
-
const productId = productIdForPlan(planId);
|
|
183
|
-
|
|
184
|
-
if (!productId) {
|
|
185
|
-
return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
const session = await dodo.checkoutSessions.create({
|
|
190
|
-
product_cart: [{ product_id: productId, quantity: 1 }],
|
|
191
|
-
customer: { email: user.email, name: user.name },
|
|
192
|
-
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/return`,
|
|
193
|
-
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
|
|
194
|
-
metadata: { app_user_id: user.id },
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
if (!session.checkout_url) {
|
|
198
|
-
return NextResponse.json({ error: 'No checkout URL returned' }, { status: 502 });
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return NextResponse.json({ checkoutUrl: session.checkout_url });
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error('Checkout session creation failed', error);
|
|
204
|
-
return NextResponse.json({ error: 'Could not start checkout' }, { status: 500 });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
`checkout_url` is nullable — it is absent when the session is created with `payment_method_id` — so guard
|
|
210
|
-
it rather than redirecting to `undefined`. Checkout URLs are single-use and normally expire after 24 hours.
|
|
211
|
-
|
|
212
|
-
Use `checkoutSessions.create`. `payments.create` and `subscriptions.create` are deprecated for new
|
|
213
|
-
integrations. Checkout parameters are covered in depth in the `checkout-integration` skill.
|
|
214
|
-
|
|
215
|
-
### 4. Client component
|
|
216
|
-
|
|
217
|
-
```tsx
|
|
218
|
-
// components/pricing-plans.tsx
|
|
219
|
-
'use client';
|
|
220
|
-
|
|
221
|
-
import { useState } from 'react';
|
|
222
|
-
import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
|
|
223
|
-
import { PLANS } from '@/lib/plans';
|
|
224
|
-
|
|
225
|
-
export function PricingPlans() {
|
|
226
|
-
const [error, setError] = useState<string | null>(null);
|
|
227
|
-
|
|
228
|
-
async function handlePlanSelect(planId: string) {
|
|
229
|
-
setError(null);
|
|
230
|
-
try {
|
|
231
|
-
const response = await fetch('/api/checkout', {
|
|
232
|
-
method: 'POST',
|
|
233
|
-
headers: { 'Content-Type': 'application/json' },
|
|
234
|
-
body: JSON.stringify({ planId }),
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
if (!response.ok) {
|
|
238
|
-
setError('Could not start checkout. Please try again.');
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const { checkoutUrl } = (await response.json()) as { checkoutUrl: string };
|
|
243
|
-
window.location.href = checkoutUrl;
|
|
244
|
-
} catch {
|
|
245
|
-
setError('Network error. Please try again.');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return (
|
|
250
|
-
<>
|
|
251
|
-
<PricingTableOne
|
|
252
|
-
plans={PLANS}
|
|
253
|
-
title="Choose your plan"
|
|
254
|
-
description="Select the plan that works best for you"
|
|
255
|
-
onPlanSelect={handlePlanSelect}
|
|
256
|
-
theme="classic"
|
|
257
|
-
size="medium"
|
|
258
|
-
/>
|
|
259
|
-
{error ? <p role="alert">{error}</p> : null}
|
|
260
|
-
</>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Render `<PricingPlans />` from `app/pricing/page.tsx`. Nothing secret crosses into the client component:
|
|
266
|
-
it sends a plan slug and receives a URL.
|
|
111
|
+
- Plan catalogue
|
|
112
|
+
- Product id map — server only
|
|
113
|
+
- Checkout route
|
|
114
|
+
- Client component
|
|
267
115
|
|
|
268
116
|
## Subscription management and customer portal
|
|
269
117
|
|
|
@@ -458,14 +306,14 @@ signature verification is covered in the `webhook-integration` skill.
|
|
|
458
306
|
**Accepting a raw `product_id` from the request body.** A caller can then check out against any product in
|
|
459
307
|
your catalogue, including internal or discounted ones. Map an opaque plan slug to a `pdt_` id server-side.
|
|
460
308
|
|
|
461
|
-
**Hardcoding prices in the client.** `
|
|
309
|
+
**Hardcoding prices in the client.** `plans[].monthlyPrice` / `yearlyPrice` are display text. When you change a price in the Dodo
|
|
462
310
|
dashboard, the checkout charges the new amount while the pricing page keeps advertising the old one.
|
|
463
|
-
Reconcile `lib/
|
|
311
|
+
Reconcile `lib/billingsdk-config.ts` with the dashboard as part of any pricing change.
|
|
464
312
|
|
|
465
313
|
**`npm install billingsdk`.** The root repository package is private. Install through
|
|
466
314
|
`@billingsdk/cli` or the shadcn registry instead.
|
|
467
315
|
|
|
468
|
-
**Guessing prop names.**
|
|
316
|
+
**Guessing prop names.** Check the block's page on billingsdk.com/docs/components, then open the
|
|
469
317
|
generated file under `components/billingsdk/` and read the exported props type before wiring anything else.
|
|
470
318
|
|
|
471
319
|
**Using `DODO_PAYMENTS_WEBHOOK_SECRET`.** The SDK reads `DODO_PAYMENTS_WEBHOOK_KEY` for the `webhookKey`
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# BillingSDK: Pricing page
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### 1. Plan catalogue
|
|
5
|
+
|
|
6
|
+
`pricing-table-one` takes BillingSDK's `Plan[]`, not an arbitrary shape. Adding the component generates
|
|
7
|
+
`lib/billingsdk-config.ts`, which exports the `Plan` interface and a sample `plans` array — edit that array
|
|
8
|
+
rather than defining your own type. Prices are display **strings** (`monthlyPrice`, `yearlyPrice`) and
|
|
9
|
+
`features` are objects. The amount actually charged comes from the Dodo product catalogue (always in the
|
|
10
|
+
smallest currency unit), so treat this file as a mirror of the dashboard and reconcile it when you change a
|
|
11
|
+
price.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// lib/billingsdk-config.ts (generated by `add pricing-table-one`; edit the plans)
|
|
15
|
+
export interface Plan {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
highlight?: boolean;
|
|
20
|
+
type?: 'monthly' | 'yearly';
|
|
21
|
+
currency?: string;
|
|
22
|
+
monthlyPrice: string;
|
|
23
|
+
yearlyPrice: string;
|
|
24
|
+
buttonText: string;
|
|
25
|
+
badge?: string;
|
|
26
|
+
features: {
|
|
27
|
+
name: string;
|
|
28
|
+
icon: string;
|
|
29
|
+
iconColor?: string;
|
|
30
|
+
}[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const plans: Plan[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 'starter',
|
|
36
|
+
title: 'Starter',
|
|
37
|
+
description: 'For individuals getting started.',
|
|
38
|
+
currency: '$',
|
|
39
|
+
monthlyPrice: '9',
|
|
40
|
+
yearlyPrice: '90',
|
|
41
|
+
buttonText: 'Choose Starter',
|
|
42
|
+
features: [
|
|
43
|
+
{ name: '100 requests', icon: 'check' },
|
|
44
|
+
{ name: 'Basic support', icon: 'check' },
|
|
45
|
+
{ name: '1 project', icon: 'check' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'pro',
|
|
50
|
+
title: 'Pro',
|
|
51
|
+
description: 'For teams running in production.',
|
|
52
|
+
currency: '$',
|
|
53
|
+
monthlyPrice: '29',
|
|
54
|
+
yearlyPrice: '290',
|
|
55
|
+
buttonText: 'Choose Pro',
|
|
56
|
+
badge: 'Most popular',
|
|
57
|
+
highlight: true,
|
|
58
|
+
features: [
|
|
59
|
+
{ name: 'Unlimited requests', icon: 'check' },
|
|
60
|
+
{ name: 'Priority support', icon: 'check' },
|
|
61
|
+
{ name: '10 projects', icon: 'check' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The generated file is the contract for your installed version; if its `Plan` differs from the above, follow
|
|
68
|
+
the file. `onPlanSelect` receives only the plan `id`, so the ids here are the slugs the server maps below.
|
|
69
|
+
|
|
70
|
+
This example sells **monthly plans only**: `yearlyPrice` is required by the `Plan` type and is displayed,
|
|
71
|
+
but the server map below has only monthly products. To sell yearly too, create the yearly products in Dodo,
|
|
72
|
+
track the table's billing-period toggle in your own state (it is not passed to `onPlanSelect`), send it with
|
|
73
|
+
the plan id, and key the server map by plan and period (for example `pro:yearly`).
|
|
74
|
+
|
|
75
|
+
### 2. Product id map — server only
|
|
76
|
+
|
|
77
|
+
Never let the browser choose an arbitrary `product_id`. Map the public plan slug to a `pdt_` id on the
|
|
78
|
+
server and reject anything else.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// lib/plan-products.server.ts
|
|
82
|
+
import 'server-only';
|
|
83
|
+
|
|
84
|
+
// Keys must match the `id`s in lib/billingsdk-config.ts.
|
|
85
|
+
const PRODUCT_IDS: Readonly<Record<string, string>> = {
|
|
86
|
+
starter: 'pdt_starter_monthly',
|
|
87
|
+
pro: 'pdt_pro_monthly',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function productIdForPlan(value: unknown): string | null {
|
|
91
|
+
if (typeof value !== 'string' || !Object.hasOwn(PRODUCT_IDS, value)) return null;
|
|
92
|
+
return PRODUCT_IDS[value];
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 3. Checkout route
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// app/api/checkout/route.ts
|
|
100
|
+
import { NextResponse } from 'next/server';
|
|
101
|
+
import { dodo } from '@/lib/dodopayments';
|
|
102
|
+
import { productIdForPlan } from '@/lib/plan-products.server';
|
|
103
|
+
import { getCurrentUser } from '@/lib/auth';
|
|
104
|
+
|
|
105
|
+
export async function POST(request: Request) {
|
|
106
|
+
const user = await getCurrentUser();
|
|
107
|
+
if (!user) {
|
|
108
|
+
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const body: unknown = await request.json();
|
|
112
|
+
const planId = (body as { planId?: unknown }).planId;
|
|
113
|
+
const productId = productIdForPlan(planId);
|
|
114
|
+
|
|
115
|
+
if (!productId) {
|
|
116
|
+
return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const session = await dodo.checkoutSessions.create({
|
|
121
|
+
product_cart: [{ product_id: productId, quantity: 1 }],
|
|
122
|
+
customer: { email: user.email, name: user.name },
|
|
123
|
+
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/return`,
|
|
124
|
+
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
|
|
125
|
+
metadata: { app_user_id: user.id },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!session.checkout_url) {
|
|
129
|
+
return NextResponse.json({ error: 'No checkout URL returned' }, { status: 502 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return NextResponse.json({ checkoutUrl: session.checkout_url });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Checkout session creation failed', error);
|
|
135
|
+
return NextResponse.json({ error: 'Could not start checkout' }, { status: 500 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`checkout_url` is nullable — it is absent when the session is created with `payment_method_id` — so guard
|
|
141
|
+
it rather than redirecting to `undefined`. Checkout URLs are single-use and normally expire after 24 hours.
|
|
142
|
+
|
|
143
|
+
Use `checkoutSessions.create`. `payments.create` and `subscriptions.create` are deprecated for new
|
|
144
|
+
integrations. Checkout parameters are covered in depth in the `checkout-integration` skill.
|
|
145
|
+
|
|
146
|
+
### 4. Client component
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// components/pricing-plans.tsx
|
|
150
|
+
'use client';
|
|
151
|
+
|
|
152
|
+
import { useState } from 'react';
|
|
153
|
+
import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
|
|
154
|
+
import { plans } from '@/lib/billingsdk-config';
|
|
155
|
+
|
|
156
|
+
export function PricingPlans() {
|
|
157
|
+
const [error, setError] = useState<string | null>(null);
|
|
158
|
+
|
|
159
|
+
async function handlePlanSelect(planId: string) {
|
|
160
|
+
setError(null);
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch('/api/checkout', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
body: JSON.stringify({ planId }),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
setError('Could not start checkout. Please try again.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { checkoutUrl } = (await response.json()) as { checkoutUrl: string };
|
|
174
|
+
window.location.href = checkoutUrl;
|
|
175
|
+
} catch {
|
|
176
|
+
setError('Network error. Please try again.');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<>
|
|
182
|
+
<PricingTableOne
|
|
183
|
+
plans={plans}
|
|
184
|
+
title="Choose your plan"
|
|
185
|
+
description="Select the plan that works best for you"
|
|
186
|
+
onPlanSelect={handlePlanSelect}
|
|
187
|
+
theme="classic"
|
|
188
|
+
size="medium"
|
|
189
|
+
/>
|
|
190
|
+
{error ? <p role="alert">{error}</p> : null}
|
|
191
|
+
</>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Render `<PricingPlans />` from `app/pricing/page.tsx`. Nothing secret crosses into the client component:
|
|
197
|
+
it sends a plan slug and receives a URL.
|