executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
mobile-checkoutDodo 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.
Independent GitHub snapshot · Automated classification · Original documentation governs
This skill covers integrating Dodo Payments hosted checkout into native and cross-platform mobile apps using secure system browser contexts.
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.
client.checkoutSessions.create(...) and return the checkout_url to your mobile app.Full guide: references/react-native.md.
Covers:
Full guide: references/flutter.md.
Covers:
Full guide: references/ios.md.
Covers:
Full guide: references/android.md.
Covers:
Always create checkout sessions on your backend. Never embed your API key in the mobile app.
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
const MOBILE_PRODUCTS = new Map([
['starter', 'pdt_starter123'],
['pro', 'pdt_pro456'],
]);
app.post('/api/mobile-checkout', requireAuth, async (req, res) => {
const productId = MOBILE_PRODUCTS.get(req.body.plan);
if (!productId) {
return res.status(400).json({ error: 'Invalid plan' });
}
// requireAuth derives this mapping from the authenticated server-side session.
const customerId = req.auth.dodoCustomerId;
const session = await client.checkoutSessions.create({
product_cart: [{ product_id: productId, quantity: 1 }],
customer: { customer_id: customerId },
return_url: 'myapp://checkout/return',
});
res.json({ checkout_url: session.checkout_url });
});
Never grant access based on the mobile SDK result alone. Always verify via webhook or API.
Listen for payment.succeeded webhooks. Webhook signature verification is covered in the webhook-integration skill.
// Mount with express.raw({ type: 'application/json' }) so req.body is the raw Buffer.
app.post('/webhook', async (req, res) => {
let event;
try {
event = client.webhooks.unwrap(req.body.toString(), {
headers: {
'webhook-id': req.headers['webhook-id'] as string,
'webhook-signature': req.headers['webhook-signature'] as string,
'webhook-timestamp': req.headers['webhook-timestamp'] as string,
},
});
} catch {
return res.status(401).json({ error: 'Invalid signature' });
}
try {
// Dodo retries and may redeliver: claim webhook-id with a UNIQUE insert
// and grant in the same transaction so a duplicate is a no-op.
await db.$transaction(async (tx) => {
const claim = await tx.webhookLog.createMany({
data: [{ webhookId: req.headers['webhook-id'] as string, eventType: event.type }],
skipDuplicates: true,
});
if (claim.count === 0) return; // already processed
if (event.type === 'payment.succeeded') {
await grantAccess(event.data.customer.customer_id, tx);
}
});
} catch (error) {
// Non-2xx makes Dodo retry; the transaction rolled back the claim.
return res.status(500).json({ error: 'Processing failed' });
}
res.json({ received: true });
});
Query the checkout session to confirm payment:
const session = await client.checkoutSessions.retrieve(sessionId);
if (session.payment_status === 'succeeded' && session.payment_id) {
const payment = await client.payments.retrieve(session.payment_id);
await grantAccess(payment.customer.customer_id);
}
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:
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.
Never include your API key in the app binary or client-side code. Always create checkout sessions on your backend.
// WRONG
const client = new DodoPayments({
bearerToken: 'dodo_live_abc123...', // Never hardcode
});
// CORRECT
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY, // Backend only
});
The SDK result is informational. Always verify server-side before granting access.
// WRONG
if (result.status === 'succeeded') {
grantAccess(); // No verification
}
// CORRECT
if (result.status === 'succeeded') {
const verified = await verifyPaymentOnBackend(result.paymentId);
if (verified) {
grantAccess();
}
}
If you don't register the custom URL scheme, the app won't receive the return callback and checkout will appear to hang.
Info.plist and AndroidManifest.xml.Info.plist and AndroidManifest.xml.CFBundleURLTypes to Info.plist and forward URLs to DodoCheckout.handleOpenURL.manifestPlaceholders["dodoCallbackScheme"]; the SDK supplies the intent filter.Always handle all five statuses: succeeded, failed, cancelled, pending, and expired. Each requires different UX.
// WRONG
if (result.status === 'succeeded') {
showSuccess();
}
// CORRECT
switch (result.status) {
case 'succeeded':
showSuccess();
break;
case 'failed':
showFailure();
break;
case 'cancelled':
case 'pending':
// Unknown outcome - reconcile, never show a failure
reconcileAbandonedSession();
break;
case 'expired':
showExpired();
break;
}
If the app crashes or is backgrounded during checkout, the session is abandoned. Always check for and recover abandoned sessions on app startup.
// WRONG
// No recovery logic
// CORRECT: reconcile on launch and after every cancelled/pending result,
// and clear the record only once the backend reports a final outcome.
await reconcileAbandonedSession(); // defined in references/react-native.md
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.