@dodopayments/opencode-plugin 0.6.1 → 0.6.3
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/package.json +1 -1
- package/skills/billing-sdk/SKILL.md +8 -193
- package/skills/billing-sdk/references/pricing-page.md +197 -0
- package/skills/checkout-integration/SKILL.md +14 -449
- 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 +5 -151
- 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/framework-adapters/SKILL.md +38 -527
- 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 +8 -303
- package/skills/license-keys/references/cli-tool.md +143 -0
- package/skills/license-keys/references/desktop-app.md +170 -0
- package/skills/mobile-checkout/SKILL.md +25 -313
- 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/refunds-and-disputes/SKILL.md +1 -1
- package/skills/subscription-integration/SKILL.md +3 -77
- package/skills/subscription-integration/references/webhook-events.md +82 -0
- package/skills/usage-based-billing/SKILL.md +5 -114
- package/skills/usage-based-billing/references/instrumentation.md +119 -0
- package/skills/webhook-integration/SKILL.md +28 -277
- 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/package.json
CHANGED
|
@@ -77,7 +77,8 @@ From the current official registry, grouped by purpose:
|
|
|
77
77
|
|
|
78
78
|
Most blocks have a published usage example and props table on their page at
|
|
79
79
|
[billingsdk.com/docs/components](https://billingsdk.com/docs/components) (for example `subscription-management`
|
|
80
|
-
and `billing-settings`); `pricing-table-one` is reproduced
|
|
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
|
|
81
82
|
authoritative for the version you installed, so read it after running `add`.
|
|
82
83
|
|
|
83
84
|
## Server client
|
|
@@ -103,200 +104,14 @@ are `https://test.dodopayments.com` and `https://live.dodopayments.com`.
|
|
|
103
104
|
|
|
104
105
|
## Pricing page
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
Full guide: [references/pricing-page.md](references/pricing-page.md).
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
`lib/billingsdk-config.ts`, which exports the `Plan` interface and a sample `plans` array — edit that array
|
|
110
|
-
rather than defining your own type. Prices are display **strings** (`monthlyPrice`, `yearlyPrice`) and
|
|
111
|
-
`features` are objects. The amount actually charged comes from the Dodo product catalogue (always in the
|
|
112
|
-
smallest currency unit), so treat this file as a mirror of the dashboard and reconcile it when you change a
|
|
113
|
-
price.
|
|
109
|
+
Covers:
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
title: string;
|
|
120
|
-
description: string;
|
|
121
|
-
highlight?: boolean;
|
|
122
|
-
type?: 'monthly' | 'yearly';
|
|
123
|
-
currency?: string;
|
|
124
|
-
monthlyPrice: string;
|
|
125
|
-
yearlyPrice: string;
|
|
126
|
-
buttonText: string;
|
|
127
|
-
badge?: string;
|
|
128
|
-
features: {
|
|
129
|
-
name: string;
|
|
130
|
-
icon: string;
|
|
131
|
-
iconColor?: string;
|
|
132
|
-
}[];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const plans: Plan[] = [
|
|
136
|
-
{
|
|
137
|
-
id: 'starter',
|
|
138
|
-
title: 'Starter',
|
|
139
|
-
description: 'For individuals getting started.',
|
|
140
|
-
currency: '$',
|
|
141
|
-
monthlyPrice: '9',
|
|
142
|
-
yearlyPrice: '90',
|
|
143
|
-
buttonText: 'Choose Starter',
|
|
144
|
-
features: [
|
|
145
|
-
{ name: '100 requests', icon: 'check' },
|
|
146
|
-
{ name: 'Basic support', icon: 'check' },
|
|
147
|
-
{ name: '1 project', icon: 'check' },
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
id: 'pro',
|
|
152
|
-
title: 'Pro',
|
|
153
|
-
description: 'For teams running in production.',
|
|
154
|
-
currency: '$',
|
|
155
|
-
monthlyPrice: '29',
|
|
156
|
-
yearlyPrice: '290',
|
|
157
|
-
buttonText: 'Choose Pro',
|
|
158
|
-
badge: 'Most popular',
|
|
159
|
-
highlight: true,
|
|
160
|
-
features: [
|
|
161
|
-
{ name: 'Unlimited requests', icon: 'check' },
|
|
162
|
-
{ name: 'Priority support', icon: 'check' },
|
|
163
|
-
{ name: '10 projects', icon: 'check' },
|
|
164
|
-
],
|
|
165
|
-
},
|
|
166
|
-
];
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
The generated file is the contract for your installed version; if its `Plan` differs from the above, follow
|
|
170
|
-
the file. `onPlanSelect` receives only the plan `id`, so the ids here are the slugs the server maps below.
|
|
171
|
-
|
|
172
|
-
This example sells **monthly plans only**: `yearlyPrice` is required by the `Plan` type and is displayed,
|
|
173
|
-
but the server map below has only monthly products. To sell yearly too, create the yearly products in Dodo,
|
|
174
|
-
track the table's billing-period toggle in your own state (it is not passed to `onPlanSelect`), send it with
|
|
175
|
-
the plan id, and key the server map by plan and period (for example `pro:yearly`).
|
|
176
|
-
|
|
177
|
-
### 2. Product id map — server only
|
|
178
|
-
|
|
179
|
-
Never let the browser choose an arbitrary `product_id`. Map the public plan slug to a `pdt_` id on the
|
|
180
|
-
server and reject anything else.
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
// lib/plan-products.server.ts
|
|
184
|
-
import 'server-only';
|
|
185
|
-
|
|
186
|
-
// Keys must match the `id`s in lib/billingsdk-config.ts.
|
|
187
|
-
const PRODUCT_IDS: Readonly<Record<string, string>> = {
|
|
188
|
-
starter: 'pdt_starter_monthly',
|
|
189
|
-
pro: 'pdt_pro_monthly',
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
export function productIdForPlan(value: unknown): string | null {
|
|
193
|
-
if (typeof value !== 'string' || !Object.hasOwn(PRODUCT_IDS, value)) return null;
|
|
194
|
-
return PRODUCT_IDS[value];
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
### 3. Checkout route
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
// app/api/checkout/route.ts
|
|
202
|
-
import { NextResponse } from 'next/server';
|
|
203
|
-
import { dodo } from '@/lib/dodopayments';
|
|
204
|
-
import { productIdForPlan } from '@/lib/plan-products.server';
|
|
205
|
-
import { getCurrentUser } from '@/lib/auth';
|
|
206
|
-
|
|
207
|
-
export async function POST(request: Request) {
|
|
208
|
-
const user = await getCurrentUser();
|
|
209
|
-
if (!user) {
|
|
210
|
-
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const body: unknown = await request.json();
|
|
214
|
-
const planId = (body as { planId?: unknown }).planId;
|
|
215
|
-
const productId = productIdForPlan(planId);
|
|
216
|
-
|
|
217
|
-
if (!productId) {
|
|
218
|
-
return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
const session = await dodo.checkoutSessions.create({
|
|
223
|
-
product_cart: [{ product_id: productId, quantity: 1 }],
|
|
224
|
-
customer: { email: user.email, name: user.name },
|
|
225
|
-
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/return`,
|
|
226
|
-
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
|
|
227
|
-
metadata: { app_user_id: user.id },
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
if (!session.checkout_url) {
|
|
231
|
-
return NextResponse.json({ error: 'No checkout URL returned' }, { status: 502 });
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return NextResponse.json({ checkoutUrl: session.checkout_url });
|
|
235
|
-
} catch (error) {
|
|
236
|
-
console.error('Checkout session creation failed', error);
|
|
237
|
-
return NextResponse.json({ error: 'Could not start checkout' }, { status: 500 });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
`checkout_url` is nullable — it is absent when the session is created with `payment_method_id` — so guard
|
|
243
|
-
it rather than redirecting to `undefined`. Checkout URLs are single-use and normally expire after 24 hours.
|
|
244
|
-
|
|
245
|
-
Use `checkoutSessions.create`. `payments.create` and `subscriptions.create` are deprecated for new
|
|
246
|
-
integrations. Checkout parameters are covered in depth in the `checkout-integration` skill.
|
|
247
|
-
|
|
248
|
-
### 4. Client component
|
|
249
|
-
|
|
250
|
-
```tsx
|
|
251
|
-
// components/pricing-plans.tsx
|
|
252
|
-
'use client';
|
|
253
|
-
|
|
254
|
-
import { useState } from 'react';
|
|
255
|
-
import { PricingTableOne } from '@/components/billingsdk/pricing-table-one';
|
|
256
|
-
import { plans } from '@/lib/billingsdk-config';
|
|
257
|
-
|
|
258
|
-
export function PricingPlans() {
|
|
259
|
-
const [error, setError] = useState<string | null>(null);
|
|
260
|
-
|
|
261
|
-
async function handlePlanSelect(planId: string) {
|
|
262
|
-
setError(null);
|
|
263
|
-
try {
|
|
264
|
-
const response = await fetch('/api/checkout', {
|
|
265
|
-
method: 'POST',
|
|
266
|
-
headers: { 'Content-Type': 'application/json' },
|
|
267
|
-
body: JSON.stringify({ planId }),
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
if (!response.ok) {
|
|
271
|
-
setError('Could not start checkout. Please try again.');
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const { checkoutUrl } = (await response.json()) as { checkoutUrl: string };
|
|
276
|
-
window.location.href = checkoutUrl;
|
|
277
|
-
} catch {
|
|
278
|
-
setError('Network error. Please try again.');
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return (
|
|
283
|
-
<>
|
|
284
|
-
<PricingTableOne
|
|
285
|
-
plans={plans}
|
|
286
|
-
title="Choose your plan"
|
|
287
|
-
description="Select the plan that works best for you"
|
|
288
|
-
onPlanSelect={handlePlanSelect}
|
|
289
|
-
theme="classic"
|
|
290
|
-
size="medium"
|
|
291
|
-
/>
|
|
292
|
-
{error ? <p role="alert">{error}</p> : null}
|
|
293
|
-
</>
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
Render `<PricingPlans />` from `app/pricing/page.tsx`. Nothing secret crosses into the client component:
|
|
299
|
-
it sends a plan slug and receives a URL.
|
|
111
|
+
- Plan catalogue
|
|
112
|
+
- Product id map — server only
|
|
113
|
+
- Checkout route
|
|
114
|
+
- Client component
|
|
300
115
|
|
|
301
116
|
## Subscription management and customer portal
|
|
302
117
|
|
|
@@ -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.
|