@dodopayments/opencode-plugin 0.6.1 → 0.6.3

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/skills/billing-sdk/SKILL.md +8 -193
  3. package/skills/billing-sdk/references/pricing-page.md +197 -0
  4. package/skills/checkout-integration/SKILL.md +14 -449
  5. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  6. package/skills/checkout-integration/references/server-examples.md +317 -0
  7. package/skills/credit-based-billing/SKILL.md +5 -151
  8. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  9. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  10. package/skills/framework-adapters/SKILL.md +38 -527
  11. package/skills/framework-adapters/references/astro.md +52 -0
  12. package/skills/framework-adapters/references/bun.md +45 -0
  13. package/skills/framework-adapters/references/convex.md +91 -0
  14. package/skills/framework-adapters/references/express.md +72 -0
  15. package/skills/framework-adapters/references/fastify.md +54 -0
  16. package/skills/framework-adapters/references/hono.md +63 -0
  17. package/skills/framework-adapters/references/nuxt.md +53 -0
  18. package/skills/framework-adapters/references/remix.md +65 -0
  19. package/skills/framework-adapters/references/sveltekit.md +49 -0
  20. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  21. package/skills/license-keys/SKILL.md +8 -303
  22. package/skills/license-keys/references/cli-tool.md +143 -0
  23. package/skills/license-keys/references/desktop-app.md +170 -0
  24. package/skills/mobile-checkout/SKILL.md +25 -313
  25. package/skills/mobile-checkout/references/android.md +69 -0
  26. package/skills/mobile-checkout/references/flutter.md +97 -0
  27. package/skills/mobile-checkout/references/ios.md +66 -0
  28. package/skills/mobile-checkout/references/react-native.md +100 -0
  29. package/skills/refunds-and-disputes/SKILL.md +1 -1
  30. package/skills/subscription-integration/SKILL.md +3 -77
  31. package/skills/subscription-integration/references/webhook-events.md +82 -0
  32. package/skills/usage-based-billing/SKILL.md +5 -114
  33. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  34. package/skills/webhook-integration/SKILL.md +28 -277
  35. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  36. package/skills/webhook-integration/references/event-catalog.md +103 -0
  37. package/skills/webhook-integration/references/local-testing.md +42 -0
@@ -30,335 +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
- ### Installation
33
+ Full guide: [references/react-native.md](references/react-native.md).
34
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
35
+ Covers:
58
36
 
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
- // Sheet closed before the return URL arrived: the outcome is UNKNOWN and the
90
- // payment may have succeeded. Do not show a failure - reconcile instead.
91
- await reconcileAbandonedSession();
92
- break;
93
- case 'pending':
94
- // Settles later (status=processing / requires_*), or status was missing.
95
- await reconcileAbandonedSession();
96
- break;
97
- case 'expired':
98
- // Checkout session expired. Prompt user to start a new checkout.
99
- showExpired();
100
- break;
101
- }
102
- ```
103
-
104
- ### Abandoned session recovery
105
-
106
- The SDK keeps a record of the session until checkout ends with `succeeded`, `failed`, or `expired`. The record survives an app kill and **stays after a `cancelled` or `pending` result**. Check it on the next launch and after every `cancelled`/`pending`:
107
-
108
- ```typescript
109
- import { DodoCheckout } from '@dodopayments/react-native-checkout';
110
-
111
- async function reconcileAbandonedSession() {
112
- const abandoned = await DodoCheckout.getAbandonedSession();
113
- if (!abandoned) return;
114
-
115
- // Your backend calls GET /checkouts/{abandoned.sessionId} and returns payment_status.
116
- const outcome = await fetchCheckoutOutcome(abandoned.sessionId);
117
- if (outcome === 'succeeded' || outcome === 'failed' || outcome === 'expired') {
118
- showOutcome(outcome);
119
- // Clear only once the outcome is final; until then treat it as pending, not failed.
120
- await DodoCheckout.clearAbandonedSession();
121
- } else {
122
- showPending();
123
- }
124
- }
125
- ```
126
-
127
- ### Android minSdk requirement
128
-
129
- React Native checkout requires Android minSdk 24 or higher. Note: the general mobile documentation mentions minSdk 23, but React Native specifically requires 24.
37
+ - Installation
38
+ - Setup
39
+ - Starting checkout
40
+ - Abandoned session recovery
41
+ - Android minSdk requirement
130
42
 
131
43
  ## Flutter
132
44
 
133
- ### Installation
134
-
135
- Add the Dodo Payments Flutter package to `pubspec.yaml`:
136
-
137
- ```yaml
138
- dependencies:
139
- dodopayments_checkout: ^1.0.2
140
- ```
141
-
142
- ### Setup
143
-
144
- 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:
145
-
146
- ```dart
147
- import 'dart:async';
148
-
149
- import 'package:app_links/app_links.dart';
150
- import 'package:dodopayments_checkout/dodopayments_checkout.dart';
151
-
152
- late final StreamSubscription<Uri> checkoutLinkSubscription;
153
-
154
- void listenForCheckoutReturns() {
155
- checkoutLinkSubscription = AppLinks().uriLinkStream.listen((uri) {
156
- unawaited(DodoCheckout.instance.handleOpenURL(uri.toString()));
157
- });
158
- }
159
- ```
45
+ Full guide: [references/flutter.md](references/flutter.md).
160
46
 
161
- 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.
47
+ Covers:
162
48
 
163
- ### Starting checkout
164
-
165
- ```dart
166
- import 'package:dodopayments_checkout/dodopayments_checkout.dart';
167
-
168
- final result = await DodoCheckout.instance.start(
169
- CheckoutParams(
170
- checkoutUrl: Uri.parse('https://checkout.dodopayments.com/...'),
171
- returnUrl: Uri.parse('myapp://checkout/return'),
172
- onEvent: (event) => print(event.type),
173
- ),
174
- );
175
-
176
- switch (result.status) {
177
- case CheckoutStatus.succeeded:
178
- final paymentId = result.paymentId;
179
- if (paymentId != null) {
180
- await verifyPaymentOnBackend(paymentId);
181
- }
182
- showSuccess();
183
- break;
184
- case CheckoutStatus.failed:
185
- showFailure();
186
- break;
187
- case CheckoutStatus.cancelled:
188
- case CheckoutStatus.pending:
189
- // Outcome unknown (the payment may have succeeded): reconcile the
190
- // abandoned session with your backend instead of showing a failure.
191
- await reconcileAbandonedSession();
192
- break;
193
- case CheckoutStatus.expired:
194
- showExpired();
195
- break;
196
- }
197
- ```
198
-
199
- ### Return URL registration
200
-
201
- 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:
202
-
203
- ```kotlin
204
- android {
205
- defaultConfig {
206
- minSdk = 23
207
- manifestPlaceholders["dodoCallbackScheme"] = "myapp"
208
- }
209
- }
210
- ```
211
-
212
- 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.
213
-
214
- On iOS, register the same scheme in `ios/Runner/Info.plist`:
215
-
216
- ```xml
217
- <key>CFBundleURLTypes</key>
218
- <array>
219
- <dict>
220
- <key>CFBundleURLSchemes</key>
221
- <array>
222
- <string>myapp</string>
223
- </array>
224
- </dict>
225
- </array>
226
- ```
49
+ - Installation
50
+ - Setup
51
+ - Starting checkout
52
+ - Return URL registration
227
53
 
228
54
  ## iOS (native)
229
55
 
230
- 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+.
231
-
232
- ### Installation
233
-
234
- Add `https://github.com/dodopayments/dodopayments-mobile-sdk-ios` (1.1.0 or later) in **File → Add Package Dependencies**, or in `Package.swift`:
235
-
236
- ```swift
237
- .package(url: "https://github.com/dodopayments/dodopayments-mobile-sdk-ios", from: "1.1.0")
238
- ```
239
-
240
- 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`.
241
-
242
- ### Starting checkout
243
-
244
- ```swift
245
- import DodoCheckout
246
-
247
- let result = try await DodoCheckout.start(
248
- checkoutUrl: checkoutUrl, // URL from your backend's checkout session
249
- returnUrl: URL(string: "myapp://checkout/return")!,
250
- onEvent: { event in print(event.name) } // logging only
251
- )
252
-
253
- switch result.status {
254
- case .succeeded: showSuccess(result.paymentId) // UI hint only; verify on the backend
255
- case .failed: showFailure()
256
- case .cancelled: await reconcileAbandonedSession() // outcome unknown, not a failure
257
- case .pending: await reconcileAbandonedSession()
258
- case .expired: showExpired()
259
- }
260
- ```
261
-
262
- ### Forwarding the return URL
263
-
264
- `SFSafariViewController` cannot catch its own return URL, so forward every incoming URL to the SDK:
265
-
266
- ```swift
267
- // SwiftUI
268
- .onOpenURL { url in
269
- DodoCheckout.handleOpenURL(url)
270
- }
271
- ```
56
+ Full guide: [references/ios.md](references/ios.md).
272
57
 
273
- `handleOpenURL` returns `true` only for the in-progress checkout's return URL; handle other URLs yourself.
58
+ Covers:
274
59
 
275
- ### Abandoned sessions
276
-
277
- ```swift
278
- func reconcileAbandonedSession() async {
279
- guard let abandoned = DodoCheckout.getAbandonedSession() else { return }
280
-
281
- // Your backend calls GET /checkouts/{sessionId} and returns payment_status.
282
- let outcome = await fetchCheckoutOutcome(abandoned.sessionId)
283
- switch outcome {
284
- case "succeeded", "failed", "expired":
285
- showOutcome(outcome)
286
- // Clear only once the outcome is final.
287
- DodoCheckout.clearAbandonedSession()
288
- default:
289
- showPending() // still pending - keep the record and check again later
290
- }
291
- }
292
- ```
60
+ - Installation
61
+ - Starting checkout
62
+ - Forwarding the return URL
63
+ - Abandoned sessions
293
64
 
294
65
  ## Android (native)
295
66
 
296
- 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.
67
+ Full guide: [references/android.md](references/android.md).
297
68
 
298
- ### Installation
69
+ Covers:
299
70
 
300
- ```kotlin
301
- // app/build.gradle.kts
302
- dependencies {
303
- implementation("com.dodopayments.api:checkout-android:1.1.0")
304
- }
305
-
306
- android {
307
- defaultConfig {
308
- // The SDK's manifest uses this placeholder; do not add an intent filter yourself.
309
- manifestPlaceholders["dodoCallbackScheme"] = "myapp"
310
- }
311
- }
312
- ```
313
-
314
- ### Starting checkout
315
-
316
- ```kotlin
317
- import com.dodopayments.checkout.CheckoutParams
318
- import com.dodopayments.checkout.CheckoutStatus
319
- import com.dodopayments.checkout.DodoCheckout
320
- import androidx.lifecycle.lifecycleScope
321
- import kotlinx.coroutines.launch
322
-
323
- // The activity-result launcher survives process death.
324
- private val checkoutLauncher =
325
- registerForActivityResult(DodoCheckout.contract()) { result ->
326
- when (result.status) {
327
- CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId) // UI hint only; verify on the backend
328
- CheckoutStatus.FAILED -> showFailure()
329
- // Outcome unknown, not a failure. reconcileAbandonedSession() suspends
330
- // (it calls your backend), so launch it from the lifecycle scope.
331
- CheckoutStatus.CANCELLED, CheckoutStatus.PENDING ->
332
- lifecycleScope.launch { reconcileAbandonedSession() }
333
- CheckoutStatus.EXPIRED -> showExpired()
334
- }
335
- }
336
-
337
- checkoutLauncher.launch(
338
- CheckoutParams(
339
- checkoutUrl = checkoutUrl, // from your backend's checkout session
340
- returnUrl = "myapp://checkout/return"
341
- )
342
- )
343
- ```
344
-
345
- ### Abandoned sessions
346
-
347
- ```kotlin
348
- suspend fun reconcileAbandonedSession() {
349
- val abandoned = DodoCheckout.getAbandonedSession(context) ?: return
350
-
351
- // Your backend calls GET /checkouts/{sessionId} and returns payment_status.
352
- when (val outcome = fetchCheckoutOutcome(abandoned.sessionId)) {
353
- "succeeded", "failed", "expired" -> {
354
- showOutcome(outcome)
355
- // Clear only once the outcome is final.
356
- DodoCheckout.clearAbandonedSession(context)
357
- }
358
- else -> showPending() // still pending - keep the record and check again later
359
- }
360
- }
361
- ```
71
+ - Installation
72
+ - Starting checkout
73
+ - Abandoned sessions
362
74
 
363
75
  ## Backend: Creating checkout sessions
364
76
 
@@ -553,7 +265,7 @@ If the app crashes or is backgrounded during checkout, the session is abandoned.
553
265
 
554
266
  // CORRECT: reconcile on launch and after every cancelled/pending result,
555
267
  // and clear the record only once the backend reports a final outcome.
556
- await reconcileAbandonedSession(); // defined in the React Native section above
268
+ await reconcileAbandonedSession(); // defined in references/react-native.md
557
269
  ```
558
270
 
559
271
  ## Package names
@@ -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
+ ```
@@ -0,0 +1,100 @@
1
+ # Mobile In-App Checkout: React Native (Turbo Module)
2
+
3
+
4
+ ### Installation
5
+
6
+ ```bash
7
+ npm install @dodopayments/react-native-checkout
8
+ ```
9
+
10
+ For Expo projects, add the plugin to `app.json`:
11
+
12
+ ```json
13
+ {
14
+ "expo": {
15
+ "scheme": "myapp",
16
+ "plugins": [
17
+ [
18
+ "@dodopayments/react-native-checkout",
19
+ { "scheme": "myappcheckout" }
20
+ ]
21
+ ]
22
+ }
23
+ }
24
+ ```
25
+
26
+ The plugin registers a custom URL scheme (`myappcheckout://`) that the checkout flow uses to return to your app.
27
+
28
+ ### Setup
29
+
30
+ Register the URL listener at app startup:
31
+
32
+ ```typescript
33
+ import { Linking } from 'react-native';
34
+ import { DodoCheckout } from '@dodopayments/react-native-checkout';
35
+
36
+ // Required for iOS return-URL handling
37
+ Linking.addEventListener('url', ({ url }) => DodoCheckout.handleOpenURL(url));
38
+ ```
39
+
40
+ ### Starting checkout
41
+
42
+ ```typescript
43
+ const result = await DodoCheckout.start({
44
+ checkoutUrl: 'https://checkout.dodopayments.com/...', // from your backend
45
+ returnUrl: 'myappcheckout://checkout/return', // must match registered scheme
46
+ onEvent: (e) => console.log(e.type), // optional event logging
47
+ });
48
+
49
+ switch (result.status) {
50
+ case 'succeeded':
51
+ // Payment succeeded. Verify server-side before granting access.
52
+ await verifyPaymentOnBackend(result.paymentId);
53
+ showSuccess();
54
+ break;
55
+ case 'failed':
56
+ // Payment failed. Show error to user.
57
+ showFailure();
58
+ break;
59
+ case 'cancelled':
60
+ // Sheet closed before the return URL arrived: the outcome is UNKNOWN and the
61
+ // payment may have succeeded. Do not show a failure - reconcile instead.
62
+ await reconcileAbandonedSession();
63
+ break;
64
+ case 'pending':
65
+ // Settles later (status=processing / requires_*), or status was missing.
66
+ await reconcileAbandonedSession();
67
+ break;
68
+ case 'expired':
69
+ // Checkout session expired. Prompt user to start a new checkout.
70
+ showExpired();
71
+ break;
72
+ }
73
+ ```
74
+
75
+ ### Abandoned session recovery
76
+
77
+ The SDK keeps a record of the session until checkout ends with `succeeded`, `failed`, or `expired`. The record survives an app kill and **stays after a `cancelled` or `pending` result**. Check it on the next launch and after every `cancelled`/`pending`:
78
+
79
+ ```typescript
80
+ import { DodoCheckout } from '@dodopayments/react-native-checkout';
81
+
82
+ async function reconcileAbandonedSession() {
83
+ const abandoned = await DodoCheckout.getAbandonedSession();
84
+ if (!abandoned) return;
85
+
86
+ // Your backend calls GET /checkouts/{abandoned.sessionId} and returns payment_status.
87
+ const outcome = await fetchCheckoutOutcome(abandoned.sessionId);
88
+ if (outcome === 'succeeded' || outcome === 'failed' || outcome === 'expired') {
89
+ showOutcome(outcome);
90
+ // Clear only once the outcome is final; until then treat it as pending, not failed.
91
+ await DodoCheckout.clearAbandonedSession();
92
+ } else {
93
+ showPending();
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Android minSdk requirement
99
+
100
+ React Native checkout requires Android minSdk 24 or higher. Note: the general mobile documentation mentions minSdk 23, but React Native specifically requires 24.