@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.
- package/package.json +1 -1
- package/skills/billing-sdk/SKILL.md +8 -193
- package/skills/billing-sdk/references/pricing-page.md +197 -0
- package/skills/checkout-integration/SKILL.md +14 -449
- 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 +5 -151
- 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/framework-adapters/SKILL.md +38 -527
- 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 +8 -303
- package/skills/license-keys/references/cli-tool.md +143 -0
- package/skills/license-keys/references/desktop-app.md +170 -0
- package/skills/mobile-checkout/SKILL.md +25 -313
- 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/refunds-and-disputes/SKILL.md +1 -1
- package/skills/subscription-integration/SKILL.md +3 -77
- package/skills/subscription-integration/references/webhook-events.md +82 -0
- package/skills/usage-based-billing/SKILL.md +5 -114
- package/skills/usage-based-billing/references/instrumentation.md +119 -0
- package/skills/webhook-integration/SKILL.md +28 -277
- 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
|
@@ -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
|
-
|
|
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
|
|
35
|
+
Covers:
|
|
58
36
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
Covers:
|
|
162
48
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
Covers:
|
|
274
59
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
67
|
+
Full guide: [references/android.md](references/android.md).
|
|
297
68
|
|
|
298
|
-
|
|
69
|
+
Covers:
|
|
299
70
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
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.
|