@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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/opencode-plugin/README.md +1 -1
  3. package/package.json +1 -1
  4. package/skills/better-auth-integration/SKILL.md +12 -3
  5. package/skills/billing-sdk/SKILL.md +15 -167
  6. package/skills/billing-sdk/references/pricing-page.md +197 -0
  7. package/skills/checkout-integration/SKILL.md +31 -460
  8. package/skills/checkout-integration/references/overlay-checkout.md +148 -0
  9. package/skills/checkout-integration/references/server-examples.md +317 -0
  10. package/skills/credit-based-billing/SKILL.md +11 -158
  11. package/skills/credit-based-billing/references/ai-token-example.md +108 -0
  12. package/skills/credit-based-billing/references/credit-webhooks.md +51 -0
  13. package/skills/customer-management/SKILL.md +25 -17
  14. package/skills/discounts-and-promotions/SKILL.md +20 -17
  15. package/skills/dodo-best-practices/SKILL.md +18 -5
  16. package/skills/dodo-mcp-usage/SKILL.md +152 -0
  17. package/skills/framework-adapters/SKILL.md +64 -421
  18. package/skills/framework-adapters/references/astro.md +52 -0
  19. package/skills/framework-adapters/references/bun.md +45 -0
  20. package/skills/framework-adapters/references/convex.md +91 -0
  21. package/skills/framework-adapters/references/express.md +72 -0
  22. package/skills/framework-adapters/references/fastify.md +54 -0
  23. package/skills/framework-adapters/references/hono.md +63 -0
  24. package/skills/framework-adapters/references/nuxt.md +53 -0
  25. package/skills/framework-adapters/references/remix.md +65 -0
  26. package/skills/framework-adapters/references/sveltekit.md +49 -0
  27. package/skills/framework-adapters/references/tanstack-start.md +31 -0
  28. package/skills/license-keys/SKILL.md +117 -367
  29. package/skills/license-keys/references/cli-tool.md +143 -0
  30. package/skills/license-keys/references/desktop-app.md +170 -0
  31. package/skills/localized-pricing/SKILL.md +51 -29
  32. package/skills/mobile-checkout/SKILL.md +72 -310
  33. package/skills/mobile-checkout/references/android.md +69 -0
  34. package/skills/mobile-checkout/references/flutter.md +97 -0
  35. package/skills/mobile-checkout/references/ios.md +66 -0
  36. package/skills/mobile-checkout/references/react-native.md +100 -0
  37. package/skills/product-catalog-management/SKILL.md +39 -16
  38. package/skills/refunds-and-disputes/SKILL.md +91 -71
  39. package/skills/subscription-integration/SKILL.md +43 -64
  40. package/skills/subscription-integration/references/webhook-events.md +82 -0
  41. package/skills/testing-and-go-live/SKILL.md +22 -15
  42. package/skills/usage-based-billing/SKILL.md +16 -122
  43. package/skills/usage-based-billing/references/instrumentation.md +119 -0
  44. package/skills/webhook-integration/SKILL.md +31 -263
  45. package/skills/webhook-integration/references/common-mistakes.md +146 -0
  46. package/skills/webhook-integration/references/event-catalog.md +103 -0
  47. package/skills/webhook-integration/references/local-testing.md +42 -0
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mobile-checkout
3
- description: Guide for implementing mobile in-app checkout with Dodo Payments across React Native, Flutter, iOS, and Android platforms.
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
- ### 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
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
- 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
121
42
 
122
43
  ## Flutter
123
44
 
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.
45
+ Full guide: [references/flutter.md](references/flutter.md).
153
46
 
154
- ### Starting checkout
47
+ Covers:
155
48
 
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
- ```
49
+ - Installation
50
+ - Setup
51
+ - Starting checkout
52
+ - Return URL registration
218
53
 
219
54
  ## iOS (native)
220
55
 
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
56
+ Full guide: [references/ios.md](references/ios.md).
234
57
 
235
- Register your custom URL scheme in `Info.plist`:
58
+ Covers:
236
59
 
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
- ```
60
+ - Installation
61
+ - Starting checkout
62
+ - Forwarding the return URL
63
+ - Abandoned sessions
278
64
 
279
65
  ## Android (native)
280
66
 
281
- ### Setup
282
-
283
- Use Chrome Custom Tabs to open the checkout URL:
67
+ Full guide: [references/android.md](references/android.md).
284
68
 
285
- ```kotlin
286
- import androidx.browser.customtabs.CustomTabsIntent
287
- import android.net.Uri
69
+ Covers:
288
70
 
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
- ```
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
- 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);
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
- 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:
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
- - 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.
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
- 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.
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: Add an intent filter with the scheme in `AndroidManifest.xml`.
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
- showPending();
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
- const abandoned = await DodoCheckout.getAbandonedSession();
505
- if (abandoned) {
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
+ ```