@dodopayments/opencode-plugin 0.6.1 → 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/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/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
|
@@ -219,316 +219,21 @@ Optional query fields: `page_size` (default 10, max 100), `page_number` (default
|
|
|
219
219
|
|
|
220
220
|
## Desktop App Integration
|
|
221
221
|
|
|
222
|
-
|
|
222
|
+
Full guide: [references/desktop-app.md](references/desktop-app.md).
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
// main/license.ts
|
|
226
|
-
import Store from 'electron-store';
|
|
227
|
-
import DodoPayments from 'dodopayments';
|
|
228
|
-
import os from 'os';
|
|
229
|
-
|
|
230
|
-
const store = new Store();
|
|
231
|
-
const client = new DodoPayments({ bearerToken: 'public', environment: 'test_mode' }); // public endpoints: placeholder token; use 'live_mode' in production
|
|
232
|
-
|
|
233
|
-
interface LicenseInfo {
|
|
234
|
-
key: string;
|
|
235
|
-
instanceId: string;
|
|
236
|
-
activatedAt: string;
|
|
237
|
-
lastValidatedAt?: string; // last time the server confirmed the license (absent on records saved before it was added)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export async function activateLicense(licenseKey: string): Promise<boolean> {
|
|
241
|
-
try {
|
|
242
|
-
const deviceName = `${os.hostname()} - ${os.platform()}`;
|
|
243
|
-
|
|
244
|
-
const response = await client.licenses.activate({
|
|
245
|
-
license_key: licenseKey,
|
|
246
|
-
name: deviceName,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const now = new Date().toISOString();
|
|
250
|
-
const licenseInfo: LicenseInfo = {
|
|
251
|
-
key: licenseKey,
|
|
252
|
-
instanceId: response.id,
|
|
253
|
-
activatedAt: now,
|
|
254
|
-
lastValidatedAt: now,
|
|
255
|
-
};
|
|
224
|
+
Covers:
|
|
256
225
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
} catch (error) {
|
|
260
|
-
console.error('Activation failed:', error);
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export async function checkLicense(): Promise<boolean> {
|
|
266
|
-
const license = store.get('license') as LicenseInfo | undefined;
|
|
267
|
-
|
|
268
|
-
if (!license) {
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
try {
|
|
273
|
-
const response = await client.licenses.validate({
|
|
274
|
-
license_key: license.key,
|
|
275
|
-
license_key_instance_id: license.instanceId,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
if (!response.valid) {
|
|
279
|
-
// An explicit "invalid" from the server (revoked key, deactivated
|
|
280
|
-
// instance) ends the license: drop the cache so a later offline start
|
|
281
|
-
// cannot fall back to the grace period.
|
|
282
|
-
store.delete('license');
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
store.set('license', { ...license, lastValidatedAt: new Date().toISOString() });
|
|
286
|
-
return true;
|
|
287
|
-
} catch (error) {
|
|
288
|
-
// Only a connection failure (offline, DNS, timeout) earns the grace period.
|
|
289
|
-
// Any other API error - a revoked key, a deactivated instance - is a real
|
|
290
|
-
// answer from the server, so fail closed.
|
|
291
|
-
if (!(error instanceof DodoPayments.APIConnectionError)) {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Trust the cached result only within a grace period measured from the
|
|
296
|
-
// LAST SUCCESSFUL validation, not from activation - otherwise a key revoked
|
|
297
|
-
// yesterday keeps working offline for 30 days after activation, and an old
|
|
298
|
-
// activation gets no grace at all.
|
|
299
|
-
// Records saved before lastValidatedAt existed fall back to activatedAt
|
|
300
|
-
// until their first successful validation.
|
|
301
|
-
const lastValidated = new Date(license.lastValidatedAt ?? license.activatedAt);
|
|
302
|
-
const daysSinceValidation = (Date.now() - lastValidated.getTime()) / (1000 * 60 * 60 * 24);
|
|
303
|
-
|
|
304
|
-
// Allow 30-day offline grace period
|
|
305
|
-
return daysSinceValidation < 30;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export async function deactivateLicense(): Promise<boolean> {
|
|
310
|
-
const license = store.get('license') as LicenseInfo | undefined;
|
|
311
|
-
|
|
312
|
-
if (!license) {
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
await client.licenses.deactivate({
|
|
318
|
-
license_key: license.key,
|
|
319
|
-
license_key_instance_id: license.instanceId,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
store.delete('license');
|
|
323
|
-
return true;
|
|
324
|
-
} catch (error) {
|
|
325
|
-
console.error('Deactivation failed:', error);
|
|
326
|
-
return false;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### React component for license input
|
|
332
|
-
|
|
333
|
-
```tsx
|
|
334
|
-
// components/LicenseActivation.tsx
|
|
335
|
-
import { useState } from 'react';
|
|
336
|
-
|
|
337
|
-
interface Props {
|
|
338
|
-
onActivated: () => void;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export function LicenseActivation({ onActivated }: Props) {
|
|
342
|
-
const [licenseKey, setLicenseKey] = useState('');
|
|
343
|
-
const [loading, setLoading] = useState(false);
|
|
344
|
-
const [error, setError] = useState<string | null>(null);
|
|
345
|
-
|
|
346
|
-
const handleActivate = async () => {
|
|
347
|
-
setLoading(true);
|
|
348
|
-
setError(null);
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const success = await window.electronAPI.activateLicense(licenseKey);
|
|
352
|
-
|
|
353
|
-
if (success) {
|
|
354
|
-
onActivated();
|
|
355
|
-
} else {
|
|
356
|
-
setError('Invalid license key. Please check and try again.');
|
|
357
|
-
}
|
|
358
|
-
} catch (err) {
|
|
359
|
-
setError('Activation failed. Please try again.');
|
|
360
|
-
} finally {
|
|
361
|
-
setLoading(false);
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
return (
|
|
366
|
-
<div className="license-form">
|
|
367
|
-
<h2>Activate Your License</h2>
|
|
368
|
-
<p>Enter your license key to unlock all features.</p>
|
|
369
|
-
|
|
370
|
-
<input
|
|
371
|
-
type="text"
|
|
372
|
-
value={licenseKey}
|
|
373
|
-
onChange={(e) => setLicenseKey(e.target.value)}
|
|
374
|
-
placeholder="XXXX-XXXX-XXXX-XXXX"
|
|
375
|
-
disabled={loading}
|
|
376
|
-
/>
|
|
377
|
-
|
|
378
|
-
{error && <p className="error">{error}</p>}
|
|
379
|
-
|
|
380
|
-
<button onClick={handleActivate} disabled={loading || !licenseKey}>
|
|
381
|
-
{loading ? 'Activating...' : 'Activate License'}
|
|
382
|
-
</button>
|
|
383
|
-
</div>
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
---
|
|
226
|
+
- Electron app example
|
|
227
|
+
- React component for license input
|
|
389
228
|
|
|
390
229
|
## CLI Tool Integration
|
|
391
230
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
// src/license.ts
|
|
396
|
-
import Conf from 'conf';
|
|
397
|
-
import DodoPayments from 'dodopayments';
|
|
398
|
-
import { machineIdSync } from 'node-machine-id';
|
|
399
|
-
|
|
400
|
-
interface StoredLicense {
|
|
401
|
-
key: string;
|
|
402
|
-
instanceId: string;
|
|
403
|
-
machineId: string;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
interface CliStore {
|
|
407
|
-
license?: StoredLicense;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Typing the store keeps `config.get('license')` strongly typed at every call site.
|
|
411
|
-
const config = new Conf<CliStore>({ projectName: 'your-cli' });
|
|
412
|
-
const client = new DodoPayments({ bearerToken: 'public', environment: 'test_mode' }); // public endpoints: placeholder token; use 'live_mode' in production
|
|
231
|
+
Full guide: [references/cli-tool.md](references/cli-tool.md).
|
|
413
232
|
|
|
414
|
-
|
|
415
|
-
const machineId = machineIdSync();
|
|
416
|
-
const deviceName = `CLI - ${process.platform} - ${machineId.substring(0, 8)}`;
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
const response = await client.licenses.activate({
|
|
420
|
-
license_key: licenseKey,
|
|
421
|
-
name: deviceName,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
config.set('license', {
|
|
425
|
-
key: licenseKey,
|
|
426
|
-
instanceId: response.id,
|
|
427
|
-
machineId,
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
console.log('License activated successfully!');
|
|
431
|
-
} catch (error: any) {
|
|
432
|
-
if (error.status === 422) {
|
|
433
|
-
console.error('Activation limit reached. Deactivate another device first.');
|
|
434
|
-
} else {
|
|
435
|
-
console.error('Activation failed:', error.message);
|
|
436
|
-
}
|
|
437
|
-
process.exit(1);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
export async function checkLicense(): Promise<boolean> {
|
|
442
|
-
const license = config.get('license');
|
|
443
|
-
|
|
444
|
-
if (!license) {
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
try {
|
|
449
|
-
const response = await client.licenses.validate({
|
|
450
|
-
license_key: license.key,
|
|
451
|
-
license_key_instance_id: license.instanceId,
|
|
452
|
-
});
|
|
233
|
+
Covers:
|
|
453
234
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export async function deactivate(): Promise<void> {
|
|
461
|
-
const license = config.get('license');
|
|
462
|
-
|
|
463
|
-
if (!license) {
|
|
464
|
-
console.log('No active license found.');
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
await client.licenses.deactivate({
|
|
470
|
-
license_key: license.key,
|
|
471
|
-
license_key_instance_id: license.instanceId,
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
config.delete('license');
|
|
475
|
-
console.log('License deactivated.');
|
|
476
|
-
} catch (error: any) {
|
|
477
|
-
console.error('Deactivation failed:', error.message);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function requireLicense() {
|
|
482
|
-
return async () => {
|
|
483
|
-
const valid = await checkLicense();
|
|
484
|
-
if (!valid) {
|
|
485
|
-
console.error('This command requires a valid license.');
|
|
486
|
-
console.error('Run: your-cli activate <license-key>');
|
|
487
|
-
process.exit(1);
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
### CLI commands
|
|
494
|
-
|
|
495
|
-
```typescript
|
|
496
|
-
// src/cli.ts
|
|
497
|
-
import { Command } from 'commander';
|
|
498
|
-
import { activate, deactivate, checkLicense, requireLicense } from './license';
|
|
499
|
-
|
|
500
|
-
const program = new Command();
|
|
501
|
-
|
|
502
|
-
program
|
|
503
|
-
.command('activate <license-key>')
|
|
504
|
-
.description('Activate your license')
|
|
505
|
-
.action(activate);
|
|
506
|
-
|
|
507
|
-
program
|
|
508
|
-
.command('deactivate')
|
|
509
|
-
.description('Deactivate license on this device')
|
|
510
|
-
.action(deactivate);
|
|
511
|
-
|
|
512
|
-
program
|
|
513
|
-
.command('status')
|
|
514
|
-
.description('Check license status')
|
|
515
|
-
.action(async () => {
|
|
516
|
-
const valid = await checkLicense();
|
|
517
|
-
console.log(valid ? 'License: Active' : 'License: Not activated');
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
program
|
|
521
|
-
.command('generate')
|
|
522
|
-
.description('Generate something (requires license)')
|
|
523
|
-
.hook('preAction', requireLicense())
|
|
524
|
-
.action(async () => {
|
|
525
|
-
// Premium feature
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
program.parse();
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
---
|
|
235
|
+
- Node.js CLI example
|
|
236
|
+
- CLI commands
|
|
532
237
|
|
|
533
238
|
## Webhook Integration
|
|
534
239
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Dodo Payments License Keys: CLI Tool Integration
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Node.js CLI example
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
// src/license.ts
|
|
8
|
+
import Conf from 'conf';
|
|
9
|
+
import DodoPayments from 'dodopayments';
|
|
10
|
+
import { machineIdSync } from 'node-machine-id';
|
|
11
|
+
|
|
12
|
+
interface StoredLicense {
|
|
13
|
+
key: string;
|
|
14
|
+
instanceId: string;
|
|
15
|
+
machineId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CliStore {
|
|
19
|
+
license?: StoredLicense;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Typing the store keeps `config.get('license')` strongly typed at every call site.
|
|
23
|
+
const config = new Conf<CliStore>({ projectName: 'your-cli' });
|
|
24
|
+
const client = new DodoPayments({ bearerToken: 'public', environment: 'test_mode' }); // public endpoints: placeholder token; use 'live_mode' in production
|
|
25
|
+
|
|
26
|
+
export async function activate(licenseKey: string): Promise<void> {
|
|
27
|
+
const machineId = machineIdSync();
|
|
28
|
+
const deviceName = `CLI - ${process.platform} - ${machineId.substring(0, 8)}`;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await client.licenses.activate({
|
|
32
|
+
license_key: licenseKey,
|
|
33
|
+
name: deviceName,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
config.set('license', {
|
|
37
|
+
key: licenseKey,
|
|
38
|
+
instanceId: response.id,
|
|
39
|
+
machineId,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
console.log('License activated successfully!');
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
if (error.status === 422) {
|
|
45
|
+
console.error('Activation limit reached. Deactivate another device first.');
|
|
46
|
+
} else {
|
|
47
|
+
console.error('Activation failed:', error.message);
|
|
48
|
+
}
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function checkLicense(): Promise<boolean> {
|
|
54
|
+
const license = config.get('license');
|
|
55
|
+
|
|
56
|
+
if (!license) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await client.licenses.validate({
|
|
62
|
+
license_key: license.key,
|
|
63
|
+
license_key_instance_id: license.instanceId,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return response.valid;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function deactivate(): Promise<void> {
|
|
73
|
+
const license = config.get('license');
|
|
74
|
+
|
|
75
|
+
if (!license) {
|
|
76
|
+
console.log('No active license found.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await client.licenses.deactivate({
|
|
82
|
+
license_key: license.key,
|
|
83
|
+
license_key_instance_id: license.instanceId,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
config.delete('license');
|
|
87
|
+
console.log('License deactivated.');
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
console.error('Deactivation failed:', error.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function requireLicense() {
|
|
94
|
+
return async () => {
|
|
95
|
+
const valid = await checkLicense();
|
|
96
|
+
if (!valid) {
|
|
97
|
+
console.error('This command requires a valid license.');
|
|
98
|
+
console.error('Run: your-cli activate <license-key>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### CLI commands
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// src/cli.ts
|
|
109
|
+
import { Command } from 'commander';
|
|
110
|
+
import { activate, deactivate, checkLicense, requireLicense } from './license';
|
|
111
|
+
|
|
112
|
+
const program = new Command();
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command('activate <license-key>')
|
|
116
|
+
.description('Activate your license')
|
|
117
|
+
.action(activate);
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command('deactivate')
|
|
121
|
+
.description('Deactivate license on this device')
|
|
122
|
+
.action(deactivate);
|
|
123
|
+
|
|
124
|
+
program
|
|
125
|
+
.command('status')
|
|
126
|
+
.description('Check license status')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
const valid = await checkLicense();
|
|
129
|
+
console.log(valid ? 'License: Active' : 'License: Not activated');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('generate')
|
|
134
|
+
.description('Generate something (requires license)')
|
|
135
|
+
.hook('preAction', requireLicense())
|
|
136
|
+
.action(async () => {
|
|
137
|
+
// Premium feature
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
program.parse();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Dodo Payments License Keys: Desktop App Integration
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Electron app example
|
|
5
|
+
|
|
6
|
+
```typescript
|
|
7
|
+
// main/license.ts
|
|
8
|
+
import Store from 'electron-store';
|
|
9
|
+
import DodoPayments from 'dodopayments';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
|
|
12
|
+
const store = new Store();
|
|
13
|
+
const client = new DodoPayments({ bearerToken: 'public', environment: 'test_mode' }); // public endpoints: placeholder token; use 'live_mode' in production
|
|
14
|
+
|
|
15
|
+
interface LicenseInfo {
|
|
16
|
+
key: string;
|
|
17
|
+
instanceId: string;
|
|
18
|
+
activatedAt: string;
|
|
19
|
+
lastValidatedAt?: string; // last time the server confirmed the license (absent on records saved before it was added)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function activateLicense(licenseKey: string): Promise<boolean> {
|
|
23
|
+
try {
|
|
24
|
+
const deviceName = `${os.hostname()} - ${os.platform()}`;
|
|
25
|
+
|
|
26
|
+
const response = await client.licenses.activate({
|
|
27
|
+
license_key: licenseKey,
|
|
28
|
+
name: deviceName,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
const licenseInfo: LicenseInfo = {
|
|
33
|
+
key: licenseKey,
|
|
34
|
+
instanceId: response.id,
|
|
35
|
+
activatedAt: now,
|
|
36
|
+
lastValidatedAt: now,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
store.set('license', licenseInfo);
|
|
40
|
+
return true;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Activation failed:', error);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function checkLicense(): Promise<boolean> {
|
|
48
|
+
const license = store.get('license') as LicenseInfo | undefined;
|
|
49
|
+
|
|
50
|
+
if (!license) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const response = await client.licenses.validate({
|
|
56
|
+
license_key: license.key,
|
|
57
|
+
license_key_instance_id: license.instanceId,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!response.valid) {
|
|
61
|
+
// An explicit "invalid" from the server (revoked key, deactivated
|
|
62
|
+
// instance) ends the license: drop the cache so a later offline start
|
|
63
|
+
// cannot fall back to the grace period.
|
|
64
|
+
store.delete('license');
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
store.set('license', { ...license, lastValidatedAt: new Date().toISOString() });
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Only a connection failure (offline, DNS, timeout) earns the grace period.
|
|
71
|
+
// Any other API error - a revoked key, a deactivated instance - is a real
|
|
72
|
+
// answer from the server, so fail closed.
|
|
73
|
+
if (!(error instanceof DodoPayments.APIConnectionError)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Trust the cached result only within a grace period measured from the
|
|
78
|
+
// LAST SUCCESSFUL validation, not from activation - otherwise a key revoked
|
|
79
|
+
// yesterday keeps working offline for 30 days after activation, and an old
|
|
80
|
+
// activation gets no grace at all.
|
|
81
|
+
// Records saved before lastValidatedAt existed fall back to activatedAt
|
|
82
|
+
// until their first successful validation.
|
|
83
|
+
const lastValidated = new Date(license.lastValidatedAt ?? license.activatedAt);
|
|
84
|
+
const daysSinceValidation = (Date.now() - lastValidated.getTime()) / (1000 * 60 * 60 * 24);
|
|
85
|
+
|
|
86
|
+
// Allow 30-day offline grace period
|
|
87
|
+
return daysSinceValidation < 30;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function deactivateLicense(): Promise<boolean> {
|
|
92
|
+
const license = store.get('license') as LicenseInfo | undefined;
|
|
93
|
+
|
|
94
|
+
if (!license) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await client.licenses.deactivate({
|
|
100
|
+
license_key: license.key,
|
|
101
|
+
license_key_instance_id: license.instanceId,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
store.delete('license');
|
|
105
|
+
return true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Deactivation failed:', error);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### React component for license input
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// components/LicenseActivation.tsx
|
|
117
|
+
import { useState } from 'react';
|
|
118
|
+
|
|
119
|
+
interface Props {
|
|
120
|
+
onActivated: () => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function LicenseActivation({ onActivated }: Props) {
|
|
124
|
+
const [licenseKey, setLicenseKey] = useState('');
|
|
125
|
+
const [loading, setLoading] = useState(false);
|
|
126
|
+
const [error, setError] = useState<string | null>(null);
|
|
127
|
+
|
|
128
|
+
const handleActivate = async () => {
|
|
129
|
+
setLoading(true);
|
|
130
|
+
setError(null);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const success = await window.electronAPI.activateLicense(licenseKey);
|
|
134
|
+
|
|
135
|
+
if (success) {
|
|
136
|
+
onActivated();
|
|
137
|
+
} else {
|
|
138
|
+
setError('Invalid license key. Please check and try again.');
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
setError('Activation failed. Please try again.');
|
|
142
|
+
} finally {
|
|
143
|
+
setLoading(false);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="license-form">
|
|
149
|
+
<h2>Activate Your License</h2>
|
|
150
|
+
<p>Enter your license key to unlock all features.</p>
|
|
151
|
+
|
|
152
|
+
<input
|
|
153
|
+
type="text"
|
|
154
|
+
value={licenseKey}
|
|
155
|
+
onChange={(e) => setLicenseKey(e.target.value)}
|
|
156
|
+
placeholder="XXXX-XXXX-XXXX-XXXX"
|
|
157
|
+
disabled={loading}
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
{error && <p className="error">{error}</p>}
|
|
161
|
+
|
|
162
|
+
<button onClick={handleActivate} disabled={loading || !licenseKey}>
|
|
163
|
+
{loading ? 'Activating...' : 'Activate License'}
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|