@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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/billing-sdk/SKILL.md +8 -193
  3. package/skills/billing-sdk/references/pricing-page.md +197 -0
  4. package/skills/checkout-integration/SKILL.md +14 -449
  5. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  6. package/skills/checkout-integration/references/server-examples.md +317 -0
  7. package/skills/credit-based-billing/SKILL.md +5 -151
  8. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  9. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  10. package/skills/framework-adapters/SKILL.md +38 -527
  11. package/skills/framework-adapters/references/astro.md +52 -0
  12. package/skills/framework-adapters/references/bun.md +45 -0
  13. package/skills/framework-adapters/references/convex.md +91 -0
  14. package/skills/framework-adapters/references/express.md +72 -0
  15. package/skills/framework-adapters/references/fastify.md +54 -0
  16. package/skills/framework-adapters/references/hono.md +63 -0
  17. package/skills/framework-adapters/references/nuxt.md +53 -0
  18. package/skills/framework-adapters/references/remix.md +65 -0
  19. package/skills/framework-adapters/references/sveltekit.md +49 -0
  20. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  21. package/skills/license-keys/SKILL.md +8 -303
  22. package/skills/license-keys/references/cli-tool.md +143 -0
  23. package/skills/license-keys/references/desktop-app.md +170 -0
  24. package/skills/mobile-checkout/SKILL.md +25 -313
  25. package/skills/mobile-checkout/references/android.md +69 -0
  26. package/skills/mobile-checkout/references/flutter.md +97 -0
  27. package/skills/mobile-checkout/references/ios.md +66 -0
  28. package/skills/mobile-checkout/references/react-native.md +100 -0
  29. package/skills/refunds-and-disputes/SKILL.md +1 -1
  30. package/skills/subscription-integration/SKILL.md +3 -77
  31. package/skills/subscription-integration/references/webhook-events.md +82 -0
  32. package/skills/usage-based-billing/SKILL.md +5 -114
  33. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  34. package/skills/webhook-integration/SKILL.md +28 -277
  35. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  36. package/skills/webhook-integration/references/event-catalog.md +103 -0
  37. package/skills/webhook-integration/references/local-testing.md +42 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dodopayments/opencode-plugin",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Dodo Payments agent skills and MCP servers for OpenCode.",
5
5
  "type": "module",
6
6
  "main": "opencode-plugin/index.js",
@@ -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 below. The generated file's props type is still
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
- ### 1. Plan catalogue
107
+ Full guide: [references/pricing-page.md](references/pricing-page.md).
107
108
 
108
- `pricing-table-one` takes BillingSDK's `Plan[]`, not an arbitrary shape. Adding the component generates
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
- ```typescript
116
- // lib/billingsdk-config.ts (generated by `add pricing-table-one`; edit the plans)
117
- export interface Plan {
118
- id: string;
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.