@dodopayments/opencode-plugin 0.3.2 → 0.4.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 +4 -2
- package/README.md +92 -12
- package/opencode-plugin/README.md +22 -3
- package/opencode-plugin/index.js +19 -12
- package/package.json +1 -1
- package/skills/best-practices/SKILL.md +335 -179
- package/skills/better-auth-integration/SKILL.md +214 -0
- package/skills/billing-sdk/SKILL.md +357 -370
- package/skills/checkout-integration/SKILL.md +580 -329
- package/skills/credit-based-billing/SKILL.md +448 -775
- package/skills/customer-management/SKILL.md +281 -0
- package/skills/discounts-and-promotions/SKILL.md +464 -0
- package/skills/framework-adapters/SKILL.md +644 -0
- package/skills/license-keys/SKILL.md +324 -199
- package/skills/localized-pricing/SKILL.md +230 -0
- package/skills/mobile-checkout/SKILL.md +521 -0
- package/skills/product-catalog-management/SKILL.md +311 -0
- package/skills/refunds-and-disputes/SKILL.md +333 -0
- package/skills/subscription-integration/SKILL.md +323 -404
- package/skills/testing-and-go-live/SKILL.md +335 -0
- package/skills/usage-based-billing/SKILL.md +339 -479
- package/skills/webhook-integration/SKILL.md +600 -402
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobile-checkout
|
|
3
|
+
description: Guide for implementing mobile in-app checkout with Dodo Payments across React Native, Flutter, iOS, and Android platforms.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mobile In-App Checkout
|
|
7
|
+
|
|
8
|
+
This skill covers integrating Dodo Payments hosted checkout into native and cross-platform mobile apps using secure system browser contexts.
|
|
9
|
+
|
|
10
|
+
## When to use this skill
|
|
11
|
+
|
|
12
|
+
- Building a React Native app with Turbo Module checkout integration
|
|
13
|
+
- Adding checkout to a Flutter app via native bridge
|
|
14
|
+
- Implementing native iOS or Android checkout with secure browser contexts
|
|
15
|
+
- Registering custom URL schemes and deep links for payment return
|
|
16
|
+
- Handling abandoned checkout sessions and recovery flows
|
|
17
|
+
- Confirming payment authority server-side before granting access
|
|
18
|
+
|
|
19
|
+
## Core principle: Backend creates, mobile opens
|
|
20
|
+
|
|
21
|
+
Your backend creates the checkout session and returns a URL. The mobile app opens that URL in a secure browser context. Your API key must never be embedded in the app binary. The mobile SDK result is informational only; always verify the payment server-side via webhook or API before unlocking features or granting access.
|
|
22
|
+
|
|
23
|
+
## Architecture overview
|
|
24
|
+
|
|
25
|
+
1. **Backend:** Create a checkout session via `client.checkoutSessions.create(...)` and return the `checkout_url` to your mobile app.
|
|
26
|
+
2. **Mobile app:** Call the platform-specific SDK with the checkout URL and a registered return URL scheme.
|
|
27
|
+
3. **Browser context:** The SDK opens the URL in a secure system browser: SFSafariViewController on iOS and Chrome Custom Tabs on Android.
|
|
28
|
+
4. **Return:** After payment, the browser navigates to your return URL. The SDK captures the result and passes it to your app.
|
|
29
|
+
5. **Verification:** Query the checkout session or listen for a webhook to confirm the payment before granting access.
|
|
30
|
+
|
|
31
|
+
## React Native (Turbo Module)
|
|
32
|
+
|
|
33
|
+
### Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
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
|
|
119
|
+
|
|
120
|
+
React Native checkout requires Android minSdk 24 or higher. Note: the general mobile documentation mentions minSdk 23, but React Native specifically requires 24.
|
|
121
|
+
|
|
122
|
+
## Flutter
|
|
123
|
+
|
|
124
|
+
### Installation
|
|
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.
|
|
153
|
+
|
|
154
|
+
### Starting checkout
|
|
155
|
+
|
|
156
|
+
```dart
|
|
157
|
+
import 'package:dodopayments_checkout/dodopayments_checkout.dart';
|
|
158
|
+
|
|
159
|
+
final result = await DodoCheckout.instance.start(
|
|
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
|
+
```
|
|
218
|
+
|
|
219
|
+
## iOS (native)
|
|
220
|
+
|
|
221
|
+
### Setup
|
|
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
|
|
234
|
+
|
|
235
|
+
Register your custom URL scheme in `Info.plist`:
|
|
236
|
+
|
|
237
|
+
```xml
|
|
238
|
+
<key>CFBundleURLTypes</key>
|
|
239
|
+
<array>
|
|
240
|
+
<dict>
|
|
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
|
+
```
|
|
278
|
+
|
|
279
|
+
## Android (native)
|
|
280
|
+
|
|
281
|
+
### Setup
|
|
282
|
+
|
|
283
|
+
Use Chrome Custom Tabs to open the checkout URL:
|
|
284
|
+
|
|
285
|
+
```kotlin
|
|
286
|
+
import androidx.browser.customtabs.CustomTabsIntent
|
|
287
|
+
import android.net.Uri
|
|
288
|
+
|
|
289
|
+
val checkoutUri = Uri.parse("https://checkout.dodopayments.com/...")
|
|
290
|
+
val customTabsIntent = CustomTabsIntent.Builder().build()
|
|
291
|
+
customTabsIntent.launchUrl(context, checkoutUri)
|
|
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
|
+
```
|
|
328
|
+
|
|
329
|
+
## Backend: Creating checkout sessions
|
|
330
|
+
|
|
331
|
+
Always create checkout sessions on your backend. Never embed your API key in the mobile app.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import DodoPayments from 'dodopayments';
|
|
335
|
+
|
|
336
|
+
const client = new DodoPayments({
|
|
337
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
338
|
+
environment: 'test_mode',
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const MOBILE_PRODUCTS = new Map([
|
|
342
|
+
['starter', 'pdt_starter123'],
|
|
343
|
+
['pro', 'pdt_pro456'],
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
app.post('/api/mobile-checkout', requireAuth, async (req, res) => {
|
|
347
|
+
const productId = MOBILE_PRODUCTS.get(req.body.plan);
|
|
348
|
+
|
|
349
|
+
if (!productId) {
|
|
350
|
+
return res.status(400).json({ error: 'Invalid plan' });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// requireAuth derives this mapping from the authenticated server-side session.
|
|
354
|
+
const customerId = req.auth.dodoCustomerId;
|
|
355
|
+
|
|
356
|
+
const session = await client.checkoutSessions.create({
|
|
357
|
+
product_cart: [{ product_id: productId, quantity: 1 }],
|
|
358
|
+
customer: { customer_id: customerId },
|
|
359
|
+
return_url: 'myapp://checkout/return',
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
res.json({ checkout_url: session.checkout_url });
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Verifying payment server-side
|
|
367
|
+
|
|
368
|
+
Never grant access based on the mobile SDK result alone. Always verify via webhook or API.
|
|
369
|
+
|
|
370
|
+
### Via webhook
|
|
371
|
+
|
|
372
|
+
Listen for `payment.succeeded` webhooks. Webhook signature verification is covered in the `webhook-integration` skill.
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
app.post('/webhook', async (req, res) => {
|
|
376
|
+
const event = client.webhooks.unwrap(req.body.toString(), {
|
|
377
|
+
headers: {
|
|
378
|
+
'webhook-id': req.headers['webhook-id'] as string,
|
|
379
|
+
'webhook-signature': req.headers['webhook-signature'] as string,
|
|
380
|
+
'webhook-timestamp': req.headers['webhook-timestamp'] as string,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (event.type === 'payment.succeeded') {
|
|
385
|
+
const paymentId = event.data.payment_id;
|
|
386
|
+
const customerId = event.data.customer.customer_id;
|
|
387
|
+
|
|
388
|
+
// Grant access to the customer
|
|
389
|
+
await grantAccess(customerId);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
res.json({ received: true });
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Via API
|
|
397
|
+
|
|
398
|
+
Query the checkout session to confirm payment:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const session = await client.checkoutSessions.retrieve(sessionId);
|
|
402
|
+
|
|
403
|
+
if (session.payment_status === 'succeeded' && session.payment_id) {
|
|
404
|
+
const payment = await client.payments.retrieve(session.payment_id);
|
|
405
|
+
await grantAccess(payment.customer.customer_id);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Selling digital goods on iOS
|
|
410
|
+
|
|
411
|
+
If you're selling digital goods (software, in-app features, subscriptions) on iOS, Apple requires you to use in-app purchase APIs for certain categories. Dodo Payments can handle the payment processing, but you must comply with App Store guidelines:
|
|
412
|
+
|
|
413
|
+
- Digital content (ebooks, music, software) must use in-app purchase.
|
|
414
|
+
- Physical goods and services can use alternative payment methods.
|
|
415
|
+
- Subscriptions for digital content must use in-app purchase.
|
|
416
|
+
|
|
417
|
+
Consult Apple's App Store Review Guidelines and consider whether your product category requires in-app purchase. If it does, integrate StoreKit 2 alongside Dodo Payments for compliance.
|
|
418
|
+
|
|
419
|
+
## Common mistakes
|
|
420
|
+
|
|
421
|
+
### Embedding the API key in the app
|
|
422
|
+
|
|
423
|
+
Never include your API key in the app binary or client-side code. Always create checkout sessions on your backend.
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
// WRONG
|
|
427
|
+
const client = new DodoPayments({
|
|
428
|
+
bearerToken: 'dodo_live_abc123...', // Never hardcode
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// CORRECT
|
|
432
|
+
const client = new DodoPayments({
|
|
433
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY, // Backend only
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Trusting the mobile SDK result
|
|
438
|
+
|
|
439
|
+
The SDK result is informational. Always verify server-side before granting access.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// WRONG
|
|
443
|
+
if (result.status === 'succeeded') {
|
|
444
|
+
grantAccess(); // No verification
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// CORRECT
|
|
448
|
+
if (result.status === 'succeeded') {
|
|
449
|
+
const verified = await verifyPaymentOnBackend(result.paymentId);
|
|
450
|
+
if (verified) {
|
|
451
|
+
grantAccess();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Forgetting URL scheme registration
|
|
457
|
+
|
|
458
|
+
If you don't register the custom URL scheme, the app won't receive the return callback and checkout will appear to hang.
|
|
459
|
+
|
|
460
|
+
- React Native: Use the Expo plugin or manually register in `Info.plist` and `AndroidManifest.xml`.
|
|
461
|
+
- Flutter: Register in both `Info.plist` and `AndroidManifest.xml`.
|
|
462
|
+
- iOS: Add `CFBundleURLTypes` to `Info.plist`.
|
|
463
|
+
- Android: Add an intent filter with the scheme in `AndroidManifest.xml`.
|
|
464
|
+
|
|
465
|
+
### Not handling all result statuses
|
|
466
|
+
|
|
467
|
+
Always handle all five statuses: `succeeded`, `failed`, `cancelled`, `pending`, and `expired`. Each requires different UX.
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// WRONG
|
|
471
|
+
if (result.status === 'succeeded') {
|
|
472
|
+
showSuccess();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// CORRECT
|
|
476
|
+
switch (result.status) {
|
|
477
|
+
case 'succeeded':
|
|
478
|
+
showSuccess();
|
|
479
|
+
break;
|
|
480
|
+
case 'failed':
|
|
481
|
+
showFailure();
|
|
482
|
+
break;
|
|
483
|
+
case 'cancelled':
|
|
484
|
+
dismiss();
|
|
485
|
+
break;
|
|
486
|
+
case 'pending':
|
|
487
|
+
showPending();
|
|
488
|
+
break;
|
|
489
|
+
case 'expired':
|
|
490
|
+
showExpired();
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Ignoring abandoned sessions
|
|
496
|
+
|
|
497
|
+
If the app crashes or is backgrounded during checkout, the session is abandoned. Always check for and recover abandoned sessions on app startup.
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// WRONG
|
|
501
|
+
// No recovery logic
|
|
502
|
+
|
|
503
|
+
// CORRECT
|
|
504
|
+
const abandoned = await DodoCheckout.getAbandonedSession();
|
|
505
|
+
if (abandoned) {
|
|
506
|
+
// Reconcile and clear
|
|
507
|
+
await DodoCheckout.clearAbandonedSession();
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Package names
|
|
512
|
+
|
|
513
|
+
Use `@dodopayments/react-native-checkout` for React Native and `dodopayments_checkout` for Flutter. The similarly named `@dodopayments/react-native` and `dodo_payments_flutter` packages do not exist.
|
|
514
|
+
|
|
515
|
+
## Resources
|
|
516
|
+
|
|
517
|
+
- [Mobile Integration](https://docs.dodopayments.com/developer-resources/mobile-integration)
|
|
518
|
+
- [React Native SDK](https://docs.dodopayments.com/developer-resources/sdks/react-native)
|
|
519
|
+
- [Flutter SDK](https://pub.dev/packages/dodopayments_checkout)
|
|
520
|
+
- [Selling Digital Goods on iOS](https://docs.dodopayments.com/features/appstore-digital-goods)
|
|
521
|
+
- [Webhook Integration](https://docs.dodopayments.com/developer-resources/webhooks/intents) (for payment verification)
|