@dodopayments/opencode-plugin 0.2.0
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/.mcp.json +22 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/opencode-plugin/README.md +42 -0
- package/opencode-plugin/index.js +30 -0
- package/opencode.json +28 -0
- package/package.json +53 -0
- package/skills/best-practices/SKILL.md +315 -0
- package/skills/billing-sdk/SKILL.md +501 -0
- package/skills/checkout-integration/SKILL.md +599 -0
- package/skills/credit-based-billing/SKILL.md +942 -0
- package/skills/license-keys/SKILL.md +591 -0
- package/skills/subscription-integration/SKILL.md +571 -0
- package/skills/usage-based-billing/SKILL.md +680 -0
- package/skills/webhook-integration/SKILL.md +529 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: license-keys
|
|
3
|
+
description: Guide for implementing license key management with Dodo Payments - activation, validation, and access control for software products.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dodo Payments License Keys
|
|
7
|
+
|
|
8
|
+
**Reference: [docs.dodopayments.com/features/license-keys](https://docs.dodopayments.com/features/license-keys)**
|
|
9
|
+
|
|
10
|
+
License keys authorize access to your digital products. Use them for software licensing, per-seat controls, and gating premium features.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
License keys are unique tokens that:
|
|
17
|
+
- Authorize access to software, plugins, CLIs
|
|
18
|
+
- Limit activations per user or device
|
|
19
|
+
- Gate downloads, updates, or premium features
|
|
20
|
+
- Can be linked to subscriptions or one-time purchases
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Creating License Keys
|
|
25
|
+
|
|
26
|
+
### In Dashboard
|
|
27
|
+
|
|
28
|
+
1. Go to Dashboard → License Keys
|
|
29
|
+
2. Click "Create License Key"
|
|
30
|
+
3. Configure settings:
|
|
31
|
+
- **Expiry Date**: Duration or "no expiry" for perpetual
|
|
32
|
+
- **Activation Limit**: Max concurrent activations (1, 5, unlimited)
|
|
33
|
+
- **Activation Instructions**: Steps for customers
|
|
34
|
+
|
|
35
|
+
4. Save the license key configuration
|
|
36
|
+
|
|
37
|
+
### Auto-Generation on Purchase
|
|
38
|
+
|
|
39
|
+
License keys can be automatically generated when a product is purchased:
|
|
40
|
+
|
|
41
|
+
1. Configure your product with license key settings
|
|
42
|
+
2. When purchased, a key is generated and emailed to customer
|
|
43
|
+
3. `license_key.created` webhook is fired
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## API Reference
|
|
48
|
+
|
|
49
|
+
### Public Endpoints (No API Key Required)
|
|
50
|
+
|
|
51
|
+
These endpoints can be called directly from client applications:
|
|
52
|
+
|
|
53
|
+
| Endpoint | Description |
|
|
54
|
+
|----------|-------------|
|
|
55
|
+
| `POST /licenses/activate` | Activate a license key |
|
|
56
|
+
| `POST /licenses/deactivate` | Deactivate an instance |
|
|
57
|
+
| `POST /licenses/validate` | Check if key is valid |
|
|
58
|
+
|
|
59
|
+
### Authenticated Endpoints (API Key Required)
|
|
60
|
+
|
|
61
|
+
| Endpoint | Description |
|
|
62
|
+
|----------|-------------|
|
|
63
|
+
| `GET /license_keys` | List all license keys |
|
|
64
|
+
| `GET /license_keys/:id` | Get license key details |
|
|
65
|
+
| `PATCH /license_keys/:id` | Update license key |
|
|
66
|
+
| `GET /license_key_instances` | List activation instances |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Implementation Examples
|
|
71
|
+
|
|
72
|
+
### Activate a License Key
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import DodoPayments from 'dodopayments';
|
|
76
|
+
|
|
77
|
+
// No API key needed for public endpoints
|
|
78
|
+
const client = new DodoPayments();
|
|
79
|
+
|
|
80
|
+
async function activateLicense(licenseKey: string, deviceName: string) {
|
|
81
|
+
try {
|
|
82
|
+
const response = await client.licenses.activate({
|
|
83
|
+
license_key: licenseKey,
|
|
84
|
+
name: deviceName, // e.g., "John's MacBook Pro"
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
instanceId: response.id,
|
|
90
|
+
message: 'License activated successfully',
|
|
91
|
+
};
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
message: error.message || 'Activation failed',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Validate a License Key
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import DodoPayments from 'dodopayments';
|
|
105
|
+
|
|
106
|
+
const client = new DodoPayments();
|
|
107
|
+
|
|
108
|
+
async function validateLicense(licenseKey: string) {
|
|
109
|
+
try {
|
|
110
|
+
const response = await client.licenses.validate({
|
|
111
|
+
license_key: licenseKey,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
valid: response.valid,
|
|
116
|
+
activations: response.activations_count,
|
|
117
|
+
maxActivations: response.activations_limit,
|
|
118
|
+
expiresAt: response.expires_at,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return { valid: false };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Deactivate a License
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import DodoPayments from 'dodopayments';
|
|
130
|
+
|
|
131
|
+
const client = new DodoPayments();
|
|
132
|
+
|
|
133
|
+
async function deactivateLicense(licenseKey: string, instanceId: string) {
|
|
134
|
+
try {
|
|
135
|
+
await client.licenses.deactivate({
|
|
136
|
+
license_key: licenseKey,
|
|
137
|
+
license_key_instance_id: instanceId,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return { success: true, message: 'License deactivated' };
|
|
141
|
+
} catch (error: any) {
|
|
142
|
+
return { success: false, message: error.message };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Desktop App Integration
|
|
150
|
+
|
|
151
|
+
### Electron App Example
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// main/license.ts
|
|
155
|
+
import Store from 'electron-store';
|
|
156
|
+
import DodoPayments from 'dodopayments';
|
|
157
|
+
|
|
158
|
+
const store = new Store();
|
|
159
|
+
const client = new DodoPayments();
|
|
160
|
+
|
|
161
|
+
interface LicenseInfo {
|
|
162
|
+
key: string;
|
|
163
|
+
instanceId: string;
|
|
164
|
+
activatedAt: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function activateLicense(licenseKey: string): Promise<boolean> {
|
|
168
|
+
try {
|
|
169
|
+
// Get device identifier
|
|
170
|
+
const deviceName = `${os.hostname()} - ${os.platform()}`;
|
|
171
|
+
|
|
172
|
+
const response = await client.licenses.activate({
|
|
173
|
+
license_key: licenseKey,
|
|
174
|
+
name: deviceName,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Store license info locally
|
|
178
|
+
const licenseInfo: LicenseInfo = {
|
|
179
|
+
key: licenseKey,
|
|
180
|
+
instanceId: response.id,
|
|
181
|
+
activatedAt: new Date().toISOString(),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
store.set('license', licenseInfo);
|
|
185
|
+
return true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('Activation failed:', error);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function checkLicense(): Promise<boolean> {
|
|
193
|
+
const license = store.get('license') as LicenseInfo | undefined;
|
|
194
|
+
|
|
195
|
+
if (!license) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = await client.licenses.validate({
|
|
201
|
+
license_key: license.key,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return response.valid;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// If offline, trust local license (with optional grace period)
|
|
207
|
+
const activatedAt = new Date(license.activatedAt);
|
|
208
|
+
const daysSinceActivation = (Date.now() - activatedAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
209
|
+
|
|
210
|
+
// Allow 30-day offline grace period
|
|
211
|
+
return daysSinceActivation < 30;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function deactivateLicense(): Promise<boolean> {
|
|
216
|
+
const license = store.get('license') as LicenseInfo | undefined;
|
|
217
|
+
|
|
218
|
+
if (!license) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await client.licenses.deactivate({
|
|
224
|
+
license_key: license.key,
|
|
225
|
+
license_key_instance_id: license.instanceId,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
store.delete('license');
|
|
229
|
+
return true;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Deactivation failed:', error);
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### React Component for License Input
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
// components/LicenseActivation.tsx
|
|
241
|
+
import { useState } from 'react';
|
|
242
|
+
|
|
243
|
+
interface Props {
|
|
244
|
+
onActivated: () => void;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function LicenseActivation({ onActivated }: Props) {
|
|
248
|
+
const [licenseKey, setLicenseKey] = useState('');
|
|
249
|
+
const [loading, setLoading] = useState(false);
|
|
250
|
+
const [error, setError] = useState<string | null>(null);
|
|
251
|
+
|
|
252
|
+
const handleActivate = async () => {
|
|
253
|
+
setLoading(true);
|
|
254
|
+
setError(null);
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
// Call main process (Electron IPC)
|
|
258
|
+
const success = await window.electronAPI.activateLicense(licenseKey);
|
|
259
|
+
|
|
260
|
+
if (success) {
|
|
261
|
+
onActivated();
|
|
262
|
+
} else {
|
|
263
|
+
setError('Invalid license key. Please check and try again.');
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
setError('Activation failed. Please try again.');
|
|
267
|
+
} finally {
|
|
268
|
+
setLoading(false);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className="license-form">
|
|
274
|
+
<h2>Activate Your License</h2>
|
|
275
|
+
<p>Enter your license key to unlock all features.</p>
|
|
276
|
+
|
|
277
|
+
<input
|
|
278
|
+
type="text"
|
|
279
|
+
value={licenseKey}
|
|
280
|
+
onChange={(e) => setLicenseKey(e.target.value)}
|
|
281
|
+
placeholder="XXXX-XXXX-XXXX-XXXX"
|
|
282
|
+
disabled={loading}
|
|
283
|
+
/>
|
|
284
|
+
|
|
285
|
+
{error && <p className="error">{error}</p>}
|
|
286
|
+
|
|
287
|
+
<button onClick={handleActivate} disabled={loading || !licenseKey}>
|
|
288
|
+
{loading ? 'Activating...' : 'Activate License'}
|
|
289
|
+
</button>
|
|
290
|
+
|
|
291
|
+
<a href="https://yoursite.com/purchase" target="_blank">
|
|
292
|
+
Don't have a license? Purchase here
|
|
293
|
+
</a>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## CLI Tool Integration
|
|
302
|
+
|
|
303
|
+
### Node.js CLI Example
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// src/license.ts
|
|
307
|
+
import Conf from 'conf';
|
|
308
|
+
import DodoPayments from 'dodopayments';
|
|
309
|
+
import { machineIdSync } from 'node-machine-id';
|
|
310
|
+
|
|
311
|
+
const config = new Conf({ projectName: 'your-cli' });
|
|
312
|
+
const client = new DodoPayments();
|
|
313
|
+
|
|
314
|
+
export async function activate(licenseKey: string): Promise<void> {
|
|
315
|
+
const machineId = machineIdSync();
|
|
316
|
+
const deviceName = `CLI - ${process.platform} - ${machineId.substring(0, 8)}`;
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const response = await client.licenses.activate({
|
|
320
|
+
license_key: licenseKey,
|
|
321
|
+
name: deviceName,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
config.set('license', {
|
|
325
|
+
key: licenseKey,
|
|
326
|
+
instanceId: response.id,
|
|
327
|
+
machineId,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
console.log('License activated successfully!');
|
|
331
|
+
} catch (error: any) {
|
|
332
|
+
if (error.status === 400) {
|
|
333
|
+
console.error('Invalid license key.');
|
|
334
|
+
} else if (error.status === 403) {
|
|
335
|
+
console.error('Activation limit reached. Deactivate another device first.');
|
|
336
|
+
} else {
|
|
337
|
+
console.error('Activation failed:', error.message);
|
|
338
|
+
}
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function checkLicense(): Promise<boolean> {
|
|
344
|
+
const license = config.get('license') as any;
|
|
345
|
+
|
|
346
|
+
if (!license) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const response = await client.licenses.validate({
|
|
352
|
+
license_key: license.key,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return response.valid;
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function deactivate(): Promise<void> {
|
|
362
|
+
const license = config.get('license') as any;
|
|
363
|
+
|
|
364
|
+
if (!license) {
|
|
365
|
+
console.log('No active license found.');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
await client.licenses.deactivate({
|
|
371
|
+
license_key: license.key,
|
|
372
|
+
license_key_instance_id: license.instanceId,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
config.delete('license');
|
|
376
|
+
console.log('License deactivated.');
|
|
377
|
+
} catch (error: any) {
|
|
378
|
+
console.error('Deactivation failed:', error.message);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Middleware to check license before commands
|
|
383
|
+
export function requireLicense() {
|
|
384
|
+
return async () => {
|
|
385
|
+
const valid = await checkLicense();
|
|
386
|
+
if (!valid) {
|
|
387
|
+
console.error('This command requires a valid license.');
|
|
388
|
+
console.error('Run: your-cli activate <license-key>');
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### CLI Commands
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// src/cli.ts
|
|
399
|
+
import { Command } from 'commander';
|
|
400
|
+
import { activate, deactivate, checkLicense, requireLicense } from './license';
|
|
401
|
+
|
|
402
|
+
const program = new Command();
|
|
403
|
+
|
|
404
|
+
program
|
|
405
|
+
.command('activate <license-key>')
|
|
406
|
+
.description('Activate your license')
|
|
407
|
+
.action(activate);
|
|
408
|
+
|
|
409
|
+
program
|
|
410
|
+
.command('deactivate')
|
|
411
|
+
.description('Deactivate license on this device')
|
|
412
|
+
.action(deactivate);
|
|
413
|
+
|
|
414
|
+
program
|
|
415
|
+
.command('status')
|
|
416
|
+
.description('Check license status')
|
|
417
|
+
.action(async () => {
|
|
418
|
+
const valid = await checkLicense();
|
|
419
|
+
console.log(valid ? 'License: Active' : 'License: Not activated');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Protected command example
|
|
423
|
+
program
|
|
424
|
+
.command('generate')
|
|
425
|
+
.description('Generate something (requires license)')
|
|
426
|
+
.hook('preAction', requireLicense())
|
|
427
|
+
.action(async () => {
|
|
428
|
+
// Premium feature
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
program.parse();
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Webhook Integration
|
|
437
|
+
|
|
438
|
+
### Handle License Key Creation
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// app/api/webhooks/dodo/route.ts
|
|
442
|
+
export async function POST(req: NextRequest) {
|
|
443
|
+
const event = await req.json();
|
|
444
|
+
|
|
445
|
+
if (event.type === 'license_key.created') {
|
|
446
|
+
const { id, key, product_id, customer_id, expires_at } = event.data;
|
|
447
|
+
|
|
448
|
+
// Store in your database
|
|
449
|
+
await prisma.license.create({
|
|
450
|
+
data: {
|
|
451
|
+
externalId: id,
|
|
452
|
+
key: key,
|
|
453
|
+
productId: product_id,
|
|
454
|
+
customerId: customer_id,
|
|
455
|
+
expiresAt: expires_at ? new Date(expires_at) : null,
|
|
456
|
+
status: 'active',
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Optional: Send custom email with activation instructions
|
|
461
|
+
await sendLicenseEmail(customer_id, key, product_id);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return NextResponse.json({ received: true });
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Server-Side Validation
|
|
471
|
+
|
|
472
|
+
For sensitive operations, validate server-side with your API key:
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
// app/api/validate-license/route.ts
|
|
476
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
477
|
+
import DodoPayments from 'dodopayments';
|
|
478
|
+
|
|
479
|
+
const client = new DodoPayments({
|
|
480
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
export async function POST(req: NextRequest) {
|
|
484
|
+
const { licenseKey } = await req.json();
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
// Get detailed license info (requires API key)
|
|
488
|
+
const licenses = await client.licenseKeys.list({
|
|
489
|
+
license_key: licenseKey,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (licenses.items.length === 0) {
|
|
493
|
+
return NextResponse.json({ valid: false, error: 'License not found' });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const license = licenses.items[0];
|
|
497
|
+
|
|
498
|
+
// Check various conditions
|
|
499
|
+
const valid =
|
|
500
|
+
license.status === 'active' &&
|
|
501
|
+
(!license.expires_at || new Date(license.expires_at) > new Date());
|
|
502
|
+
|
|
503
|
+
return NextResponse.json({
|
|
504
|
+
valid,
|
|
505
|
+
status: license.status,
|
|
506
|
+
activationsUsed: license.activations_count,
|
|
507
|
+
activationsLimit: license.activations_limit,
|
|
508
|
+
expiresAt: license.expires_at,
|
|
509
|
+
});
|
|
510
|
+
} catch (error: any) {
|
|
511
|
+
return NextResponse.json({ valid: false, error: error.message }, { status: 500 });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Best Practices
|
|
519
|
+
|
|
520
|
+
### 1. Keep Limits Clear
|
|
521
|
+
Choose sensible defaults for expiry and activations based on your product type.
|
|
522
|
+
|
|
523
|
+
### 2. Guide Users
|
|
524
|
+
Provide precise activation instructions:
|
|
525
|
+
- "Paste the key in Settings → License"
|
|
526
|
+
- "Run: `mycli activate <key>`"
|
|
527
|
+
- Include self-serve documentation links
|
|
528
|
+
|
|
529
|
+
### 3. Validate Server-Side
|
|
530
|
+
For critical access control, always validate on your server before granting access.
|
|
531
|
+
|
|
532
|
+
### 4. Handle Offline Gracefully
|
|
533
|
+
Allow a grace period for offline use in desktop/CLI apps.
|
|
534
|
+
|
|
535
|
+
### 5. Monitor Events
|
|
536
|
+
Use webhooks to detect abuse patterns and automate revocations.
|
|
537
|
+
|
|
538
|
+
### 6. Provide Easy Deactivation
|
|
539
|
+
Let users deactivate devices themselves to manage their activation slots.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## Common Patterns
|
|
544
|
+
|
|
545
|
+
### Feature Gating
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
async function canAccessFeature(feature: string, licenseKey: string) {
|
|
549
|
+
const { valid } = await validateLicense(licenseKey);
|
|
550
|
+
|
|
551
|
+
if (!valid) return false;
|
|
552
|
+
|
|
553
|
+
// Map features to license tiers
|
|
554
|
+
const featureTiers = {
|
|
555
|
+
'basic-export': ['starter', 'pro', 'enterprise'],
|
|
556
|
+
'advanced-export': ['pro', 'enterprise'],
|
|
557
|
+
'api-access': ['enterprise'],
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const license = await getLicenseDetails(licenseKey);
|
|
561
|
+
return featureTiers[feature]?.includes(license.tier);
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Subscription-Linked Licenses
|
|
566
|
+
|
|
567
|
+
When license is linked to a subscription:
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
// Handle subscription.cancelled webhook
|
|
571
|
+
if (event.type === 'subscription.cancelled') {
|
|
572
|
+
const { customer_id } = event.data;
|
|
573
|
+
|
|
574
|
+
// Disable associated license keys
|
|
575
|
+
const licenses = await client.licenseKeys.list({ customer_id });
|
|
576
|
+
|
|
577
|
+
for (const license of licenses.items) {
|
|
578
|
+
await client.licenseKeys.update(license.id, {
|
|
579
|
+
status: 'disabled',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Resources
|
|
588
|
+
|
|
589
|
+
- [License Keys Documentation](https://docs.dodopayments.com/features/license-keys)
|
|
590
|
+
- [API Reference](https://docs.dodopayments.com/api-reference/license-keys)
|
|
591
|
+
- [Video Tutorial](https://www.youtube.com/watch?v=BNuLTXok8dQ)
|