@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mobile-checkout
|
|
3
|
-
description:
|
|
3
|
+
description: Dodo Payments in-app checkout for React Native, Flutter, native iOS, and Android using SFSafariViewController or Chrome Custom Tabs. Use when a mobile app must open a backend-created checkout session, handle deep link or custom URL scheme returns, recover abandoned checkouts, and verify payment server-side before unlocking access.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Mobile In-App Checkout
|
|
@@ -30,301 +30,47 @@ Your backend creates the checkout session and returns a URL. The mobile app open
|
|
|
30
30
|
|
|
31
31
|
## React Native (Turbo Module)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Full guide: [references/react-native.md](references/react-native.md).
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
npm install @dodopayments/react-native-checkout
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
For Expo projects, add the plugin to `app.json`:
|
|
40
|
-
|
|
41
|
-
```json
|
|
42
|
-
{
|
|
43
|
-
"expo": {
|
|
44
|
-
"scheme": "myapp",
|
|
45
|
-
"plugins": [
|
|
46
|
-
[
|
|
47
|
-
"@dodopayments/react-native-checkout",
|
|
48
|
-
{ "scheme": "myappcheckout" }
|
|
49
|
-
]
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
The plugin registers a custom URL scheme (`myappcheckout://`) that the checkout flow uses to return to your app.
|
|
56
|
-
|
|
57
|
-
### Setup
|
|
58
|
-
|
|
59
|
-
Register the URL listener at app startup:
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import { Linking } from 'react-native';
|
|
63
|
-
import { DodoCheckout } from '@dodopayments/react-native-checkout';
|
|
64
|
-
|
|
65
|
-
// Required for iOS return-URL handling
|
|
66
|
-
Linking.addEventListener('url', ({ url }) => DodoCheckout.handleOpenURL(url));
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Starting checkout
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
const result = await DodoCheckout.start({
|
|
73
|
-
checkoutUrl: 'https://checkout.dodopayments.com/...', // from your backend
|
|
74
|
-
returnUrl: 'myappcheckout://checkout/return', // must match registered scheme
|
|
75
|
-
onEvent: (e) => console.log(e.type), // optional event logging
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
switch (result.status) {
|
|
79
|
-
case 'succeeded':
|
|
80
|
-
// Payment succeeded. Verify server-side before granting access.
|
|
81
|
-
await verifyPaymentOnBackend(result.paymentId);
|
|
82
|
-
showSuccess();
|
|
83
|
-
break;
|
|
84
|
-
case 'failed':
|
|
85
|
-
// Payment failed. Show error to user.
|
|
86
|
-
showFailure();
|
|
87
|
-
break;
|
|
88
|
-
case 'cancelled':
|
|
89
|
-
// User cancelled. Dismiss checkout.
|
|
90
|
-
dismiss();
|
|
91
|
-
break;
|
|
92
|
-
case 'pending':
|
|
93
|
-
// Payment is pending (e.g., awaiting 3D Secure). Show waiting state.
|
|
94
|
-
showPending();
|
|
95
|
-
break;
|
|
96
|
-
case 'expired':
|
|
97
|
-
// Checkout session expired. Prompt user to start a new checkout.
|
|
98
|
-
showExpired();
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Abandoned session recovery
|
|
104
|
-
|
|
105
|
-
If the app crashes or is backgrounded during checkout, recover the session:
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
import { DodoCheckout } from '@dodopayments/react-native-checkout';
|
|
109
|
-
|
|
110
|
-
const abandoned = await DodoCheckout.getAbandonedSession();
|
|
111
|
-
if (abandoned) {
|
|
112
|
-
// Reconcile abandoned.sessionId with your backend
|
|
113
|
-
// Decide whether to resume or start fresh
|
|
114
|
-
await DodoCheckout.clearAbandonedSession();
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Android minSdk requirement
|
|
35
|
+
Covers:
|
|
119
36
|
|
|
120
|
-
|
|
37
|
+
- Installation
|
|
38
|
+
- Setup
|
|
39
|
+
- Starting checkout
|
|
40
|
+
- Abandoned session recovery
|
|
41
|
+
- Android minSdk requirement
|
|
121
42
|
|
|
122
43
|
## Flutter
|
|
123
44
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Add the Dodo Payments Flutter package to `pubspec.yaml`:
|
|
127
|
-
|
|
128
|
-
```yaml
|
|
129
|
-
dependencies:
|
|
130
|
-
dodopayments_checkout: ^1.0.2
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Setup
|
|
134
|
-
|
|
135
|
-
The package uses `DodoCheckout.instance`. On iOS, register the return URL scheme and forward incoming links from your deep-link listener. Run `flutter pub add app_links` if you use the `app_links` approach shown here:
|
|
136
|
-
|
|
137
|
-
```dart
|
|
138
|
-
import 'dart:async';
|
|
139
|
-
|
|
140
|
-
import 'package:app_links/app_links.dart';
|
|
141
|
-
import 'package:dodopayments_checkout/dodopayments_checkout.dart';
|
|
142
|
-
|
|
143
|
-
late final StreamSubscription<Uri> checkoutLinkSubscription;
|
|
144
|
-
|
|
145
|
-
void listenForCheckoutReturns() {
|
|
146
|
-
checkoutLinkSubscription = AppLinks().uriLinkStream.listen((uri) {
|
|
147
|
-
unawaited(DodoCheckout.instance.handleOpenURL(uri.toString()));
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Start the listener from your root state object's `initState` and cancel `checkoutLinkSubscription` from `dispose`. `handleOpenURL` is required on iOS and safely returns `false` on Android.
|
|
45
|
+
Full guide: [references/flutter.md](references/flutter.md).
|
|
153
46
|
|
|
154
|
-
|
|
47
|
+
Covers:
|
|
155
48
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
CheckoutParams(
|
|
161
|
-
checkoutUrl: Uri.parse('https://checkout.dodopayments.com/...'),
|
|
162
|
-
returnUrl: Uri.parse('myapp://checkout/return'),
|
|
163
|
-
onEvent: (event) => print(event.type),
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
switch (result.status) {
|
|
168
|
-
case CheckoutStatus.succeeded:
|
|
169
|
-
final paymentId = result.paymentId;
|
|
170
|
-
if (paymentId != null) {
|
|
171
|
-
await verifyPaymentOnBackend(paymentId);
|
|
172
|
-
}
|
|
173
|
-
showSuccess();
|
|
174
|
-
break;
|
|
175
|
-
case CheckoutStatus.failed:
|
|
176
|
-
showFailure();
|
|
177
|
-
break;
|
|
178
|
-
case CheckoutStatus.cancelled:
|
|
179
|
-
dismiss();
|
|
180
|
-
break;
|
|
181
|
-
case CheckoutStatus.pending:
|
|
182
|
-
showPending();
|
|
183
|
-
break;
|
|
184
|
-
case CheckoutStatus.expired:
|
|
185
|
-
showExpired();
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### Return URL registration
|
|
191
|
-
|
|
192
|
-
On Android, set the callback scheme in `android/app/build.gradle.kts`. The package's native checkout dependency supplies the intent filter, so do not add one manually:
|
|
193
|
-
|
|
194
|
-
```kotlin
|
|
195
|
-
android {
|
|
196
|
-
defaultConfig {
|
|
197
|
-
minSdk = 23
|
|
198
|
-
manifestPlaceholders["dodoCallbackScheme"] = "myapp"
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Remove an empty `android:taskAffinity=""` from `MainActivity` if the generated Flutter manifest contains it; it can prevent Custom Tabs from returning correctly on some devices.
|
|
204
|
-
|
|
205
|
-
On iOS, register the same scheme in `ios/Runner/Info.plist`:
|
|
206
|
-
|
|
207
|
-
```xml
|
|
208
|
-
<key>CFBundleURLTypes</key>
|
|
209
|
-
<array>
|
|
210
|
-
<dict>
|
|
211
|
-
<key>CFBundleURLSchemes</key>
|
|
212
|
-
<array>
|
|
213
|
-
<string>myapp</string>
|
|
214
|
-
</array>
|
|
215
|
-
</dict>
|
|
216
|
-
</array>
|
|
217
|
-
```
|
|
49
|
+
- Installation
|
|
50
|
+
- Setup
|
|
51
|
+
- Starting checkout
|
|
52
|
+
- Return URL registration
|
|
218
53
|
|
|
219
54
|
## iOS (native)
|
|
220
55
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Use `SFSafariViewController` to open the checkout URL:
|
|
224
|
-
|
|
225
|
-
```swift
|
|
226
|
-
import SafariServices
|
|
227
|
-
|
|
228
|
-
let checkoutURL = URL(string: "https://checkout.dodopayments.com/...")!
|
|
229
|
-
let safariVC = SFSafariViewController(url: checkoutURL)
|
|
230
|
-
present(safariVC, animated: true)
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### Deep-link handling
|
|
56
|
+
Full guide: [references/ios.md](references/ios.md).
|
|
234
57
|
|
|
235
|
-
|
|
58
|
+
Covers:
|
|
236
59
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
<key>CFBundleURLSchemes</key>
|
|
242
|
-
<array>
|
|
243
|
-
<string>myapp</string>
|
|
244
|
-
</array>
|
|
245
|
-
</dict>
|
|
246
|
-
</array>
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Handle the return in your app delegate:
|
|
250
|
-
|
|
251
|
-
```swift
|
|
252
|
-
func application(
|
|
253
|
-
_ app: UIApplication,
|
|
254
|
-
open url: URL,
|
|
255
|
-
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
|
256
|
-
) -> Bool {
|
|
257
|
-
if url.scheme == "myapp" && url.host == "checkout" {
|
|
258
|
-
// Parse the result from the URL query parameters
|
|
259
|
-
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
|
260
|
-
let status = components?.queryItems?.first(where: { $0.name == "status" })?.value
|
|
261
|
-
|
|
262
|
-
switch status {
|
|
263
|
-
case "succeeded":
|
|
264
|
-
let paymentId = components?.queryItems?.first(where: { $0.name == "payment_id" })?.value
|
|
265
|
-
verifyPaymentOnBackend(paymentId: paymentId)
|
|
266
|
-
case "cancelled":
|
|
267
|
-
dismiss()
|
|
268
|
-
case "expired":
|
|
269
|
-
showExpired()
|
|
270
|
-
default:
|
|
271
|
-
break
|
|
272
|
-
}
|
|
273
|
-
return true
|
|
274
|
-
}
|
|
275
|
-
return false
|
|
276
|
-
}
|
|
277
|
-
```
|
|
60
|
+
- Installation
|
|
61
|
+
- Starting checkout
|
|
62
|
+
- Forwarding the return URL
|
|
63
|
+
- Abandoned sessions
|
|
278
64
|
|
|
279
65
|
## Android (native)
|
|
280
66
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
Use Chrome Custom Tabs to open the checkout URL:
|
|
67
|
+
Full guide: [references/android.md](references/android.md).
|
|
284
68
|
|
|
285
|
-
|
|
286
|
-
import androidx.browser.customtabs.CustomTabsIntent
|
|
287
|
-
import android.net.Uri
|
|
69
|
+
Covers:
|
|
288
70
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
### Deep-link handling
|
|
295
|
-
|
|
296
|
-
Register your custom URL scheme in `AndroidManifest.xml`:
|
|
297
|
-
|
|
298
|
-
```xml
|
|
299
|
-
<activity android:name=".CheckoutReturnActivity">
|
|
300
|
-
<intent-filter>
|
|
301
|
-
<action android:name="android.intent.action.VIEW" />
|
|
302
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
303
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
304
|
-
<data android:scheme="myapp" android:host="checkout" android:path="/return" />
|
|
305
|
-
</intent-filter>
|
|
306
|
-
</activity>
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
Handle the return in your activity:
|
|
310
|
-
|
|
311
|
-
```kotlin
|
|
312
|
-
override fun onCreate(savedInstanceState: Bundle?) {
|
|
313
|
-
super.onCreate(savedInstanceState)
|
|
314
|
-
|
|
315
|
-
val uri = intent.data
|
|
316
|
-
if (uri?.scheme == "myapp" && uri.host == "checkout") {
|
|
317
|
-
val status = uri.getQueryParameter("status")
|
|
318
|
-
val paymentId = uri.getQueryParameter("payment_id")
|
|
319
|
-
|
|
320
|
-
when (status) {
|
|
321
|
-
"succeeded" -> verifyPaymentOnBackend(paymentId)
|
|
322
|
-
"cancelled" -> dismiss()
|
|
323
|
-
"expired" -> showExpired()
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
```
|
|
71
|
+
- Installation
|
|
72
|
+
- Starting checkout
|
|
73
|
+
- Abandoned sessions
|
|
328
74
|
|
|
329
75
|
## Backend: Creating checkout sessions
|
|
330
76
|
|
|
@@ -372,23 +118,40 @@ Never grant access based on the mobile SDK result alone. Always verify via webho
|
|
|
372
118
|
Listen for `payment.succeeded` webhooks. Webhook signature verification is covered in the `webhook-integration` skill.
|
|
373
119
|
|
|
374
120
|
```typescript
|
|
121
|
+
// Mount with express.raw({ type: 'application/json' }) so req.body is the raw Buffer.
|
|
375
122
|
app.post('/webhook', async (req, res) => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
// Grant access to the customer
|
|
389
|
-
await grantAccess(customerId);
|
|
123
|
+
let event;
|
|
124
|
+
try {
|
|
125
|
+
event = client.webhooks.unwrap(req.body.toString(), {
|
|
126
|
+
headers: {
|
|
127
|
+
'webhook-id': req.headers['webhook-id'] as string,
|
|
128
|
+
'webhook-signature': req.headers['webhook-signature'] as string,
|
|
129
|
+
'webhook-timestamp': req.headers['webhook-timestamp'] as string,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
390
134
|
}
|
|
391
|
-
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
// Dodo retries and may redeliver: claim webhook-id with a UNIQUE insert
|
|
138
|
+
// and grant in the same transaction so a duplicate is a no-op.
|
|
139
|
+
await db.$transaction(async (tx) => {
|
|
140
|
+
const claim = await tx.webhookLog.createMany({
|
|
141
|
+
data: [{ webhookId: req.headers['webhook-id'] as string, eventType: event.type }],
|
|
142
|
+
skipDuplicates: true,
|
|
143
|
+
});
|
|
144
|
+
if (claim.count === 0) return; // already processed
|
|
145
|
+
|
|
146
|
+
if (event.type === 'payment.succeeded') {
|
|
147
|
+
await grantAccess(event.data.customer.customer_id, tx);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Non-2xx makes Dodo retry; the transaction rolled back the claim.
|
|
152
|
+
return res.status(500).json({ error: 'Processing failed' });
|
|
153
|
+
}
|
|
154
|
+
|
|
392
155
|
res.json({ received: true });
|
|
393
156
|
});
|
|
394
157
|
```
|
|
@@ -408,13 +171,14 @@ if (session.payment_status === 'succeeded' && session.payment_id) {
|
|
|
408
171
|
|
|
409
172
|
## Selling digital goods on iOS
|
|
410
173
|
|
|
411
|
-
|
|
174
|
+
Dodo Payments hosted checkout can sell digital goods (subscriptions, courses, downloads, SaaS plans) in an iOS app **only on App Store storefronts where Apple allows external purchases**:
|
|
412
175
|
|
|
413
|
-
-
|
|
414
|
-
-
|
|
415
|
-
-
|
|
176
|
+
- **United States:** Guideline 3.1.1(a) allows buttons and links to other purchase methods without an entitlement (subject to the Epic v. Apple proceedings).
|
|
177
|
+
- **European Union:** requires Apple's EU external purchase entitlement (the StoreKit External Purchases or Offers Entitlement from October 1, 2026) and DMA compliance.
|
|
178
|
+
- **Japan:** allowed under the Mobile Software Competition Act, following Apple's Japan-specific entitlement requirements.
|
|
179
|
+
- **South Korea is not supported** (Apple requires a native, non-web-view flow through an approved Korean PSP).
|
|
416
180
|
|
|
417
|
-
|
|
181
|
+
On other storefronts, digital goods sold inside the iOS app must use Apple in-app purchase (StoreKit). Review Apple's region-specific entitlements before enabling Dodo checkout for a storefront; unsupported flows can get the app rejected.
|
|
418
182
|
|
|
419
183
|
## Common mistakes
|
|
420
184
|
|
|
@@ -459,8 +223,8 @@ If you don't register the custom URL scheme, the app won't receive the return ca
|
|
|
459
223
|
|
|
460
224
|
- React Native: Use the Expo plugin or manually register in `Info.plist` and `AndroidManifest.xml`.
|
|
461
225
|
- Flutter: Register in both `Info.plist` and `AndroidManifest.xml`.
|
|
462
|
-
- iOS: Add `CFBundleURLTypes` to `Info.plist`.
|
|
463
|
-
- Android:
|
|
226
|
+
- iOS: Add `CFBundleURLTypes` to `Info.plist` and forward URLs to `DodoCheckout.handleOpenURL`.
|
|
227
|
+
- Android: Set `manifestPlaceholders["dodoCallbackScheme"]`; the SDK supplies the intent filter.
|
|
464
228
|
|
|
465
229
|
### Not handling all result statuses
|
|
466
230
|
|
|
@@ -481,10 +245,9 @@ switch (result.status) {
|
|
|
481
245
|
showFailure();
|
|
482
246
|
break;
|
|
483
247
|
case 'cancelled':
|
|
484
|
-
dismiss();
|
|
485
|
-
break;
|
|
486
248
|
case 'pending':
|
|
487
|
-
|
|
249
|
+
// Unknown outcome - reconcile, never show a failure
|
|
250
|
+
reconcileAbandonedSession();
|
|
488
251
|
break;
|
|
489
252
|
case 'expired':
|
|
490
253
|
showExpired();
|
|
@@ -500,12 +263,9 @@ If the app crashes or is backgrounded during checkout, the session is abandoned.
|
|
|
500
263
|
// WRONG
|
|
501
264
|
// No recovery logic
|
|
502
265
|
|
|
503
|
-
// CORRECT
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// Reconcile and clear
|
|
507
|
-
await DodoCheckout.clearAbandonedSession();
|
|
508
|
-
}
|
|
266
|
+
// CORRECT: reconcile on launch and after every cancelled/pending result,
|
|
267
|
+
// and clear the record only once the backend reports a final outcome.
|
|
268
|
+
await reconcileAbandonedSession(); // defined in references/react-native.md
|
|
509
269
|
```
|
|
510
270
|
|
|
511
271
|
## Package names
|
|
@@ -516,6 +276,8 @@ Use `@dodopayments/react-native-checkout` for React Native and `dodopayments_che
|
|
|
516
276
|
|
|
517
277
|
- [Mobile Integration](https://docs.dodopayments.com/developer-resources/mobile-integration)
|
|
518
278
|
- [React Native SDK](https://docs.dodopayments.com/developer-resources/sdks/react-native)
|
|
279
|
+
- [iOS SDK](https://docs.dodopayments.com/developer-resources/sdks/ios)
|
|
280
|
+
- [Android SDK](https://docs.dodopayments.com/developer-resources/sdks/android)
|
|
519
281
|
- [Flutter SDK](https://pub.dev/packages/dodopayments_checkout)
|
|
520
282
|
- [Selling Digital Goods on iOS](https://docs.dodopayments.com/features/appstore-digital-goods)
|
|
521
283
|
- [Webhook Integration](https://docs.dodopayments.com/developer-resources/webhooks/intents) (for payment verification)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Mobile In-App Checkout: Android (native)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Use the official Android checkout SDK (`minSdk` 23, Kotlin, Java 17). It opens Chrome Custom Tabs, declares the redirect intent filter itself, and returns a typed result.
|
|
5
|
+
|
|
6
|
+
### Installation
|
|
7
|
+
|
|
8
|
+
```kotlin
|
|
9
|
+
// app/build.gradle.kts
|
|
10
|
+
dependencies {
|
|
11
|
+
implementation("com.dodopayments.api:checkout-android:1.1.0")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
android {
|
|
15
|
+
defaultConfig {
|
|
16
|
+
// The SDK's manifest uses this placeholder; do not add an intent filter yourself.
|
|
17
|
+
manifestPlaceholders["dodoCallbackScheme"] = "myapp"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Starting checkout
|
|
23
|
+
|
|
24
|
+
```kotlin
|
|
25
|
+
import com.dodopayments.checkout.CheckoutParams
|
|
26
|
+
import com.dodopayments.checkout.CheckoutStatus
|
|
27
|
+
import com.dodopayments.checkout.DodoCheckout
|
|
28
|
+
import androidx.lifecycle.lifecycleScope
|
|
29
|
+
import kotlinx.coroutines.launch
|
|
30
|
+
|
|
31
|
+
// The activity-result launcher survives process death.
|
|
32
|
+
private val checkoutLauncher =
|
|
33
|
+
registerForActivityResult(DodoCheckout.contract()) { result ->
|
|
34
|
+
when (result.status) {
|
|
35
|
+
CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId) // UI hint only; verify on the backend
|
|
36
|
+
CheckoutStatus.FAILED -> showFailure()
|
|
37
|
+
// Outcome unknown, not a failure. reconcileAbandonedSession() suspends
|
|
38
|
+
// (it calls your backend), so launch it from the lifecycle scope.
|
|
39
|
+
CheckoutStatus.CANCELLED, CheckoutStatus.PENDING ->
|
|
40
|
+
lifecycleScope.launch { reconcileAbandonedSession() }
|
|
41
|
+
CheckoutStatus.EXPIRED -> showExpired()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
checkoutLauncher.launch(
|
|
46
|
+
CheckoutParams(
|
|
47
|
+
checkoutUrl = checkoutUrl, // from your backend's checkout session
|
|
48
|
+
returnUrl = "myapp://checkout/return"
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Abandoned sessions
|
|
54
|
+
|
|
55
|
+
```kotlin
|
|
56
|
+
suspend fun reconcileAbandonedSession() {
|
|
57
|
+
val abandoned = DodoCheckout.getAbandonedSession(context) ?: return
|
|
58
|
+
|
|
59
|
+
// Your backend calls GET /checkouts/{sessionId} and returns payment_status.
|
|
60
|
+
when (val outcome = fetchCheckoutOutcome(abandoned.sessionId)) {
|
|
61
|
+
"succeeded", "failed", "expired" -> {
|
|
62
|
+
showOutcome(outcome)
|
|
63
|
+
// Clear only once the outcome is final.
|
|
64
|
+
DodoCheckout.clearAbandonedSession(context)
|
|
65
|
+
}
|
|
66
|
+
else -> showPending() // still pending - keep the record and check again later
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Mobile In-App Checkout: Flutter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Installation
|
|
5
|
+
|
|
6
|
+
Add the Dodo Payments Flutter package to `pubspec.yaml`:
|
|
7
|
+
|
|
8
|
+
```yaml
|
|
9
|
+
dependencies:
|
|
10
|
+
dodopayments_checkout: ^1.0.2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Setup
|
|
14
|
+
|
|
15
|
+
The package uses `DodoCheckout.instance`. On iOS, register the return URL scheme and forward incoming links from your deep-link listener. Run `flutter pub add app_links` if you use the `app_links` approach shown here:
|
|
16
|
+
|
|
17
|
+
```dart
|
|
18
|
+
import 'dart:async';
|
|
19
|
+
|
|
20
|
+
import 'package:app_links/app_links.dart';
|
|
21
|
+
import 'package:dodopayments_checkout/dodopayments_checkout.dart';
|
|
22
|
+
|
|
23
|
+
late final StreamSubscription<Uri> checkoutLinkSubscription;
|
|
24
|
+
|
|
25
|
+
void listenForCheckoutReturns() {
|
|
26
|
+
checkoutLinkSubscription = AppLinks().uriLinkStream.listen((uri) {
|
|
27
|
+
unawaited(DodoCheckout.instance.handleOpenURL(uri.toString()));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Start the listener from your root state object's `initState` and cancel `checkoutLinkSubscription` from `dispose`. `handleOpenURL` is required on iOS and safely returns `false` on Android.
|
|
33
|
+
|
|
34
|
+
### Starting checkout
|
|
35
|
+
|
|
36
|
+
```dart
|
|
37
|
+
import 'package:dodopayments_checkout/dodopayments_checkout.dart';
|
|
38
|
+
|
|
39
|
+
final result = await DodoCheckout.instance.start(
|
|
40
|
+
CheckoutParams(
|
|
41
|
+
checkoutUrl: Uri.parse('https://checkout.dodopayments.com/...'),
|
|
42
|
+
returnUrl: Uri.parse('myapp://checkout/return'),
|
|
43
|
+
onEvent: (event) => print(event.type),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
switch (result.status) {
|
|
48
|
+
case CheckoutStatus.succeeded:
|
|
49
|
+
final paymentId = result.paymentId;
|
|
50
|
+
if (paymentId != null) {
|
|
51
|
+
await verifyPaymentOnBackend(paymentId);
|
|
52
|
+
}
|
|
53
|
+
showSuccess();
|
|
54
|
+
break;
|
|
55
|
+
case CheckoutStatus.failed:
|
|
56
|
+
showFailure();
|
|
57
|
+
break;
|
|
58
|
+
case CheckoutStatus.cancelled:
|
|
59
|
+
case CheckoutStatus.pending:
|
|
60
|
+
// Outcome unknown (the payment may have succeeded): reconcile the
|
|
61
|
+
// abandoned session with your backend instead of showing a failure.
|
|
62
|
+
await reconcileAbandonedSession();
|
|
63
|
+
break;
|
|
64
|
+
case CheckoutStatus.expired:
|
|
65
|
+
showExpired();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Return URL registration
|
|
71
|
+
|
|
72
|
+
On Android, set the callback scheme in `android/app/build.gradle.kts`. The package's native checkout dependency supplies the intent filter, so do not add one manually:
|
|
73
|
+
|
|
74
|
+
```kotlin
|
|
75
|
+
android {
|
|
76
|
+
defaultConfig {
|
|
77
|
+
minSdk = 23
|
|
78
|
+
manifestPlaceholders["dodoCallbackScheme"] = "myapp"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Remove an empty `android:taskAffinity=""` from `MainActivity` if the generated Flutter manifest contains it; it can prevent Custom Tabs from returning correctly on some devices.
|
|
84
|
+
|
|
85
|
+
On iOS, register the same scheme in `ios/Runner/Info.plist`:
|
|
86
|
+
|
|
87
|
+
```xml
|
|
88
|
+
<key>CFBundleURLTypes</key>
|
|
89
|
+
<array>
|
|
90
|
+
<dict>
|
|
91
|
+
<key>CFBundleURLSchemes</key>
|
|
92
|
+
<array>
|
|
93
|
+
<string>myapp</string>
|
|
94
|
+
</array>
|
|
95
|
+
</dict>
|
|
96
|
+
</array>
|
|
97
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Mobile In-App Checkout: iOS (native)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Use the official Swift package rather than parsing the return URL yourself. It presents `SFSafariViewController`, matches the return URL, and maps `status` (including `active` for subscriptions and `processing`/`requires_*` as pending) into a typed result. Requires iOS 16+ and Swift 6.2+.
|
|
5
|
+
|
|
6
|
+
### Installation
|
|
7
|
+
|
|
8
|
+
Add `https://github.com/dodopayments/dodopayments-mobile-sdk-ios` (1.1.0 or later) in **File → Add Package Dependencies**, or in `Package.swift`:
|
|
9
|
+
|
|
10
|
+
```swift
|
|
11
|
+
.package(url: "https://github.com/dodopayments/dodopayments-mobile-sdk-ios", from: "1.1.0")
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The library product is `DodoCheckout`. Register your scheme (`myapp`) under `CFBundleURLTypes` in `Info.plist`, and use the same URL as the session's `return_url`.
|
|
15
|
+
|
|
16
|
+
### Starting checkout
|
|
17
|
+
|
|
18
|
+
```swift
|
|
19
|
+
import DodoCheckout
|
|
20
|
+
|
|
21
|
+
let result = try await DodoCheckout.start(
|
|
22
|
+
checkoutUrl: checkoutUrl, // URL from your backend's checkout session
|
|
23
|
+
returnUrl: URL(string: "myapp://checkout/return")!,
|
|
24
|
+
onEvent: { event in print(event.name) } // logging only
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
switch result.status {
|
|
28
|
+
case .succeeded: showSuccess(result.paymentId) // UI hint only; verify on the backend
|
|
29
|
+
case .failed: showFailure()
|
|
30
|
+
case .cancelled: await reconcileAbandonedSession() // outcome unknown, not a failure
|
|
31
|
+
case .pending: await reconcileAbandonedSession()
|
|
32
|
+
case .expired: showExpired()
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Forwarding the return URL
|
|
37
|
+
|
|
38
|
+
`SFSafariViewController` cannot catch its own return URL, so forward every incoming URL to the SDK:
|
|
39
|
+
|
|
40
|
+
```swift
|
|
41
|
+
// SwiftUI
|
|
42
|
+
.onOpenURL { url in
|
|
43
|
+
DodoCheckout.handleOpenURL(url)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`handleOpenURL` returns `true` only for the in-progress checkout's return URL; handle other URLs yourself.
|
|
48
|
+
|
|
49
|
+
### Abandoned sessions
|
|
50
|
+
|
|
51
|
+
```swift
|
|
52
|
+
func reconcileAbandonedSession() async {
|
|
53
|
+
guard let abandoned = DodoCheckout.getAbandonedSession() else { return }
|
|
54
|
+
|
|
55
|
+
// Your backend calls GET /checkouts/{sessionId} and returns payment_status.
|
|
56
|
+
let outcome = await fetchCheckoutOutcome(abandoned.sessionId)
|
|
57
|
+
switch outcome {
|
|
58
|
+
case "succeeded", "failed", "expired":
|
|
59
|
+
showOutcome(outcome)
|
|
60
|
+
// Clear only once the outcome is final.
|
|
61
|
+
DodoCheckout.clearAbandonedSession()
|
|
62
|
+
default:
|
|
63
|
+
showPending() // still pending - keep the record and check again later
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|