关于 mobile-checkout
这项 skill 指导在原生及跨平台移动应用中接入 Dodo Payments 托管结账,涵盖安全浏览器跳转、返回链接和服务端付款验证。也说明如何处理放弃的结账会话并在验证后授予访问权限。
主要功能
- 01React Native Turbo Module 集成
- 02Flutter 原生桥接结账
- 03原生 iOS 与 Android 指南
- 04安全系统浏览器结账
- 05深链接返回处理
- 06服务端付款验证
适用场景
- 01在移动应用中添加托管结账
- 02恢复放弃的结账会话
- 03处理付款返回链接
- 04授予访问权限前验证付款
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dodopayments/ skills --skill mobile-checkout安装后在 Claude Code 中输入/mobile-checkout确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/android.mdflutter.mdios.mdreact-native.md详细介绍
移动端应用内结账
此技能介绍如何通过安全的系统浏览器上下文,将 Dodo Payments 托管结账集成到原生和跨平台移动应用中。
何时使用此技能
- 使用 Turbo Module 结账集成构建 React Native 应用
- 通过原生桥接将结账功能添加到 Flutter 应用
- 使用安全浏览器上下文实现原生 iOS 或 Android 结账
- 注册自定义 URL scheme 和深层链接以处理支付返回
- 处理已放弃的结账会话和恢复流程
- 在授予访问权限前,通过服务器端确认支付状态
核心原则:后端创建,移动端打开
后端创建结账会话并返回 URL。移动应用在安全浏览器上下文中打开该 URL。API 密钥绝不能嵌入应用二进制文件。移动 SDK 的结果仅供参考;在解锁功能或授予访问权限前,务必通过 webhook 或 API 在服务器端验证支付。
架构概览
- **后端:**通过
client.checkoutSessions.create(...)创建结账会话,并将checkout_url返回给移动应用。 - **移动应用:**调用平台专属 SDK,并传入结账 URL 和已注册的返回 URL scheme。
- **浏览器上下文:**SDK 在安全的系统浏览器中打开 URL:iOS 上使用 SFSafariViewController,Android 上使用 Chrome Custom Tabs。
- **返回:**支付完成后,浏览器会跳转到返回 URL。SDK 会捕获结果并传递给应用。
- **验证:**查询结账会话或监听 webhook,以确认支付后再授予访问权限。
React Native(Turbo Module)
完整指南:references/react-native.md。
内容包括:
- 安装
- 设置
- 启动结账
- 已放弃会话的恢复
- Android minSdk 要求
Flutter
完整指南:references/flutter.md。
内容包括:
- 安装
- 设置
- 启动结账
- 返回 URL 注册
iOS(原生)
完整指南:references/ios.md。
内容包括:
- 安装
- 启动结账
- 转发返回 URL
- 已放弃的会话
Android(原生)
完整指南:references/android.md。
内容包括:
- 安装
- 启动结账
- 已放弃的会话
后端:创建结账会话
务必在后端创建结账会话。绝不要将 API 密钥嵌入移动应用。
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 });
});
在服务器端验证支付
绝不要仅根据移动 SDK 的结果授予访问权限。务必通过 webhook 或 API 进行验证。
通过 webhook
监听 payment.succeeded webhook。有关 webhook 签名验证的说明,请参阅 webhook-integration 技能。
// 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 });
});
通过 API
查询结账会话以确认支付:
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);
}
在 iOS 上销售数字商品
Dodo Payments 托管结账只能在 Apple 允许外部购买的 App Store storefront 中,通过 iOS 应用销售数字商品(订阅、课程、下载内容、SaaS 套餐):
- 美国:《指南》3.1.1(a) 允许在无需 entitlement 的情况下添加其他购买方式的按钮和链接(受 Epic 诉 Apple 诉讼程序影响)。
- **欧盟:**需要 Apple 的欧盟外部购买 entitlement(自 2026 年 10 月 1 日起适用的 StoreKit External Purchases 或 Offers Entitlement),并且必须遵守 DMA。
- **日本:**根据《移动软件竞争法》允许,但须遵循 Apple 针对日本的 entitlement 要求。
- 韩国不支持(Apple 要求通过获准的韩国 PSP,采用原生而非 WebView 流程)。
在其他 storefront 中,iOS 应用内销售的数字商品必须使用 Apple 应用内购买(StoreKit)。为某个 storefront 启用 Dodo 结账前,请查看 Apple 针对该地区的 entitlement 要求;不受支持的流程可能导致应用被拒。
常见错误
将 API 密钥嵌入应用
绝不要将 API 密钥放入应用二进制文件或客户端代码中。务必在后端创建结账会话。
// 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
});
信任移动端 SDK 的结果
SDK 结果仅供参考。授权前,务必在服务端进行验证。
// WRONG
if (result.status === 'succeeded') {
grantAccess(); // No verification
}
// CORRECT
if (result.status === 'succeeded') {
const verified = await verifyPaymentOnBackend(result.paymentId);
if (verified) {
grantAccess();
}
}
忘记注册 URL scheme
如果未注册自定义 URL scheme,应用将无法接收返回回调,结账流程看起来会一直卡住。
- React Native:使用 Expo 插件,或手动在
Info.plist和AndroidManifest.xml中注册。 - Flutter:在
Info.plist和AndroidManifest.xml中注册。 - iOS:将
CFBundleURLTypes添加到Info.plist,并将 URL 转发给DodoCheckout.handleOpenURL。 - Android:设置
manifestPlaceholders["dodoCallbackScheme"];SDK 会提供 intent filter。
未处理所有结果状态
务必处理全部五种状态:succeeded、failed、cancelled、pending 和 expired。每种状态都需要采用不同的 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;
}
忽略已放弃的会话
如果应用在结账过程中崩溃或切换到后台,会话就会被放弃。应用启动时,务必检查并恢复已放弃的会话。
// 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
包名
React Native 使用 @dodopayments/react-native-checkout,Flutter 使用 dodopayments_checkout。名称相似的 @dodopayments/react-native 和 dodo_payments_flutter 包并不存在。