mobile-checkout について
ネイティブおよびクロスプラットフォームのモバイルアプリに、Dodo Paymentsのホスト型チェックアウトを組み込むためのスキルです。セキュアなブラウザー遷移、戻りURL、放棄されたセッションの復旧、サーバー側での支払い確認を扱います。
主な機能
- 01React Native Turbo Module連携
- 02Flutterのネイティブブリッジ決済
- 03iOS・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 スキームとディープリンクを登録する場合
- 放棄されたチェックアウトセッションとリカバリーフローを処理する場合
- アクセスを許可する前に、サーバー側で決済の正当性を確認する場合
基本原則:バックエンドが作成し、モバイルアプリが開く
バックエンドでチェックアウトセッションを作成し、URL を返します。モバイルアプリは、その URL を安全なブラウザーコンテキストで開きます。API キーをアプリのバイナリに埋め込んではいけません。モバイル SDK の結果は参考情報にすぎません。機能をロック解除したりアクセスを許可したりする前に、必ずウェブフックまたは API を介してサーバー側で決済を検証してください。
アーキテクチャの概要
- バックエンド:
client.checkoutSessions.create(...)を使用してチェックアウトセッションを作成し、checkout_urlをモバイルアプリに返します。 - モバイルアプリ: チェックアウト URL と登録済みの戻り先 URL スキームを使って、プラットフォーム固有の SDK を呼び出します。
- ブラウザーコンテキスト: SDK が URL を iOS の SFSafariViewController、Android の Chrome Custom Tabs という安全なシステムブラウザーで開きます。
- 戻り: 決済後、ブラウザーが戻り先 URL に移動します。SDK が結果を取得し、アプリに渡します。
- 検証: アクセスを許可する前に、チェックアウトセッションを照会するか、ウェブフックをリッスンして決済を確認します。
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 の結果だけに基づいてアクセスを許可してはいけません。必ずウェブフックまたは API を介して検証してください。
ウェブフックを使用する場合
payment.succeeded ウェブフックをリッスンします。ウェブフック署名の検証については、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 のホスト型チェックアウトを使用して、iOS アプリでデジタル商品(サブスクリプション、コース、ダウンロード、SaaS プラン)を販売できるのは、Apple が外部購入を許可している App Store のストアフロントに限られます。
- 米国: ガイドライン 3.1.1(a) により、エンタイトルメントなしで他の購入方法へのボタンやリンクを設置できます(Epic 対 Apple の訴訟手続きの状況に従います)。
- 欧州連合: Apple の EU 外部購入エンタイトルメント(2026 年 10 月 1 日からの StoreKit External Purchases または Offers Entitlement)と DMA への準拠が必要です。
- 日本: モバイルソフトウェア競争促進法に基づき、Apple の日本固有のエンタイトルメント要件に従えば利用できます。
- 韓国ではサポートされていません(Apple は、承認済みの韓国 PSP を介したネイティブな非 WebView フローを必須としています)。
その他のストアフロントでは、iOS アプリ内で販売するデジタル商品に Apple のアプリ内課金(StoreKit)を使用する必要があります。ストアフロントで Dodo チェックアウトを有効にする前に、Apple の地域別エンタイトルメントを確認してください。サポート対象外のフローを使用すると、アプリが却下される可能性があります。
よくある間違い
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 スキームの登録を忘れる
カスタム URL スキームを登録しないと、アプリがコールバックを受信できず、チェックアウトが停止したように見えます。
- React Native:Expo プラグインを使用するか、
Info.plistとAndroidManifest.xmlに手動で登録します。 - Flutter:
Info.plistとAndroidManifest.xmlの両方に登録します。 - iOS:
Info.plistにCFBundleURLTypesを追加し、URL をDodoCheckout.handleOpenURLに渡します。 - Android:
manifestPlaceholders["dodoCallbackScheme"]を設定します。インテントフィルターは SDK によって提供されます。
すべての結果ステータスに対応しない
成功、失敗、キャンセル、保留中、期限切れの 5 つすべてのステータスに必ず対応してください。それぞれに応じた 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 というパッケージは存在しません。