@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
|
@@ -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
|
+
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: localized-pricing
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments multi-currency pricing, covering fixed localized prices per country or currency, Adaptive Currency live FX conversion at checkout, and purchasing power parity (PPP) percentages. Use when pricing in INR, EUR, or other local currencies, offering regional or PPP discounts, or resolving billing country and currency fallback.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Localized Pricing, Adaptive Currency, and PPP
|
|
@@ -23,9 +23,9 @@ This skill covers three distinct mechanisms for pricing in multiple countries an
|
|
|
23
23
|
|---|---|---|---|
|
|
24
24
|
| **Localized Pricing** | Developer sets fixed prices per country/currency | You (via API) | Only when you update them |
|
|
25
25
|
| **Adaptive Currency** | Live FX conversion at checkout time | Dodo (live rates) | Every transaction, based on current rates |
|
|
26
|
-
| **PPP
|
|
26
|
+
| **Purchasing Power Parity (PPP)** | Charges a per-country percentage of the base price | You (business-wide country percentages in the dashboard, per-product opt-in) | Only when you change the percentages |
|
|
27
27
|
|
|
28
|
-
**Key distinction:** Localized prices are static. Adaptive currency is dynamic. PPP is a
|
|
28
|
+
**Key distinction:** Localized prices are static. Adaptive currency is dynamic. PPP is a native per-country percentage applied to the base price before Adaptive Currency converts it.
|
|
29
29
|
|
|
30
30
|
### Localized Pricing
|
|
31
31
|
|
|
@@ -41,9 +41,23 @@ Use adaptive currency when you want to offer checkout in any currency without ma
|
|
|
41
41
|
|
|
42
42
|
### PPP (Purchasing Power Parity)
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
PPP is native ([docs](https://docs.dodopayments.com/features/purchasing-power-parity)). Dodo keeps one business-wide percentage per country (30 to 100, managed in the dashboard only), and the customer pays that share of your list price based on their billing country. Each product opts in with `price.purchasing_power_parity: true` (default `false`).
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
PPP requires Adaptive Currency: enable it under **Settings → Business** first, or product opt-in is rejected. A matching localized price always wins over PPP, and discount codes apply to the PPP-reduced price.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
await client.products.create({
|
|
50
|
+
name: 'Starter Plan',
|
|
51
|
+
tax_category: 'saas',
|
|
52
|
+
price: {
|
|
53
|
+
type: 'one_time_price',
|
|
54
|
+
currency: 'USD',
|
|
55
|
+
price: 2000, // $20.00; a customer in India at 30% pays $6.00
|
|
56
|
+
discount: 0,
|
|
57
|
+
purchasing_power_parity: true,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
47
61
|
|
|
48
62
|
## Product pricing mode
|
|
49
63
|
|
|
@@ -125,24 +139,40 @@ Archived prices are soft-deleted. They no longer apply to new checkouts but rema
|
|
|
125
139
|
|
|
126
140
|
## Adaptive Currency at checkout
|
|
127
141
|
|
|
128
|
-
Enable
|
|
142
|
+
Enable Adaptive Currency in the dashboard under **Settings → Business** ([docs](https://docs.dodopayments.com/features/adaptive-currency)). Dodo then converts the base product price at live rates. Passing `billing_currency` and `billing_address.country` on a checkout session only pins the currency you charge in; if you omit them, Adaptive Currency picks the currency and country from the customer's IP address.
|
|
129
143
|
|
|
130
144
|
```typescript
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
// Server route (e.g. POST /api/checkout): create the session and return its URL
|
|
146
|
+
export async function POST() {
|
|
147
|
+
const session = await client.checkoutSessions.create({
|
|
148
|
+
product_cart: [
|
|
149
|
+
{
|
|
150
|
+
product_id: 'pdt_premium_plan',
|
|
151
|
+
quantity: 1,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
billing_currency: 'AED', // Pin the charge currency (requires Adaptive Currency)
|
|
155
|
+
billing_address: { country: 'AE' },
|
|
156
|
+
return_url: 'https://example.com/return',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return Response.json({ checkout_url: session.checkout_url });
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Never call the SDK or expose the API key in browser code. The browser only receives the URL and redirects:
|
|
141
164
|
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
```typescript
|
|
166
|
+
// Browser
|
|
167
|
+
const res = await fetch('/api/checkout', { method: 'POST' });
|
|
168
|
+
const { checkout_url } = (await res.json()) as { checkout_url: string };
|
|
169
|
+
window.location.href = checkout_url;
|
|
144
170
|
```
|
|
145
171
|
|
|
172
|
+
**Fees:** Adaptive Currency adds a tiered conversion fee (4% under $500, 3% from $500 to $1,500, 2% above $1,500) on top of the displayed price by default. Turn on **Fees Inclusive** in **Settings → Business** to absorb the fee from your settlement instead. When a localized price matches, the transaction is always treated as fees-inclusive.
|
|
173
|
+
|
|
174
|
+
**Charm Pricing:** With Adaptive Currency on, [Charm Pricing](https://docs.dodopayments.com/features/charm-pricing) can round the converted price to a clean ending such as 49.99. It requires Adaptive Currency.
|
|
175
|
+
|
|
146
176
|
Adaptive currency is also available on subscription creation and plan changes. Check the API reference for `billing_currency` support on the specific endpoint you're using.
|
|
147
177
|
|
|
148
178
|
## Billing country and currency resolution
|
|
@@ -195,7 +225,7 @@ A customer in India using a USD card will match `by_currency: USD` but not `by_c
|
|
|
195
225
|
|
|
196
226
|
### Mistake 3: Forgetting a fallback price
|
|
197
227
|
|
|
198
|
-
If you set `pricing_mode: by_country` but don't create a localized price for a customer's country, Dodo falls back to the base price
|
|
228
|
+
If you set `pricing_mode: by_country` but don't create a localized price for a customer's country, Dodo falls back to the base price: charged directly if the customer is already in your base currency, or converted through Adaptive Currency when that is enabled ([docs](https://docs.dodopayments.com/features/localized-pricing)). Without Adaptive Currency, that is the base price in the base currency, which is usually not what you want.
|
|
199
229
|
|
|
200
230
|
Always create localized prices for your target markets before enabling the mode.
|
|
201
231
|
|
|
@@ -205,17 +235,9 @@ You can use both on the same product, but they serve different purposes. Localiz
|
|
|
205
235
|
|
|
206
236
|
If you set a localized price for India and also enable adaptive currency, the localized price takes precedence for customers in India.
|
|
207
237
|
|
|
208
|
-
### Mistake 5:
|
|
209
|
-
|
|
210
|
-
PPP discounts are generated by third-party services like ParityDeals or Evendeals. Dodo does not have a built-in PPP API. You integrate PPP by accepting discount codes from these services and passing them to Dodo at checkout.
|
|
238
|
+
### Mistake 5: Reaching for a third-party tool for PPP
|
|
211
239
|
|
|
212
|
-
|
|
213
|
-
const session = await client.checkoutSessions.create({
|
|
214
|
-
product_cart: [{ product_id: 'pdt_premium_plan', quantity: 1 }],
|
|
215
|
-
discount_codes: ['PPP_DISCOUNT_CODE_FROM_PARITY_DEALS'],
|
|
216
|
-
return_url: 'https://example.com/return',
|
|
217
|
-
});
|
|
218
|
-
```
|
|
240
|
+
PPP is native: opt products in with `price.purchasing_power_parity: true` and set country percentages in the dashboard (Adaptive Currency must be on). Third-party tools like ParityDeals are only an alternative if you want their site banners; their codes are passed as ordinary `discount_codes` at checkout.
|
|
219
241
|
|
|
220
242
|
### Mistake 6: Not handling missing localized prices in production
|
|
221
243
|
|