recur-checkout について
recur-checkoutは、recur-twを使ったRecurの決済フロー実装スキルです。ホスト型、モーダル型、埋め込み型のチェックアウトに加え、Reactとサーバー側の例、商品一覧、決済結果の処理を扱います。
主な機能
- 01ホスト型・モーダル型・埋め込み型チェックアウトの実装
- 02React hooksによるチェックアウトとサブスクリプション
- 03種類別の商品一覧
- 04決済完了・失敗・キャンセルの処理
- 05サーバー側でホスト型チェックアウトセッションを作成
活用シーン
- 01サブスクリプションや単発購入の決済を追加
- 02モーダルまたは埋め込み型の決済画面を提供
- 03商品選択ページや料金ページを構築
- 04サーバーからホスト型チェックアウトセッションを作成
MCP 対応
入力ではRecurのチェックアウト実装ガイドが説明されていますが、MCPについての記載はありません。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ recur-tw/ skills --skill recur-checkoutインストール後、Claude Code で/recur-checkoutを入力して読み込まれたか確認します。
詳細
Recur チェックアウト連携
Recur のチェックアウトフローの実装を支援します。以下の内容はすべて
recur-tw の型定義(RecurContextValue、UseSubscribeResult、CheckoutOptions、
RedirectToCheckoutOptions、SubscriptionResult)に照らして確認済みです。製品 ID は
list_products / ダッシュボードから取得する CUID です(例:cmfxq8n2a0001l8yz3k5p9t7d)。サンプルの
prod_xxx はプレースホルダーです。productSlug も渡せます。
チェックアウトモード
| モード | 方法 | 最適な用途 |
|---|---|---|
| ホステッド(推奨) | useRecur().redirectToCheckout() → checkout.recur.tw | あらゆるアプリ。localhost でも動作します |
| モーダル | useSubscribe() / useRecur().checkout() とプロバイダーの checkoutMode: 'modal' | ページを離れずにすばやく購入する場合 |
| 埋め込み | 同じフックとプロバイダーの checkoutMode: 'embedded' + containerElementId | カスタムのチェックアウトページ |
checkoutMode は呼び出し側ではなく、<RecurProvider config> に設定します。プロバイダーのデフォルトは
'embedded' で、containerElementId がないとエラーになります。ポップアップを使う場合は
checkoutMode: 'modal' を設定してください。どの呼び出しにも mode: 'hosted' | 'modal' オプションはありません。
ホステッドチェックアウト(推奨)
isCheckingOut が設定されるのは checkout() のみです。redirectToCheckout() はセッションを作成して
ページ遷移するため、保留中の状態は独自に管理してください。
'use client'
import { useState } from 'react'
import { useRecur } from 'recur-tw'
function CheckoutButton({ productId }: { productId: string }) {
const { redirectToCheckout } = useRecur()
const [redirecting, setRedirecting] = useState(false)
const handleClick = async () => {
setRedirecting(true)
try {
await redirectToCheckout({
productId, // or productSlug: 'pro-plan'
successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancelUrl: `${window.location.origin}/pricing`,
customerEmail: '[email protected]', // optional, pre-fills the checkout page
customerName: 'John Doe', // optional
externalCustomerId: 'user_123', // optional, links to your user system
})
} catch (err) {
setRedirecting(false)
console.error('Failed to start checkout:', err)
}
}
return (
<button onClick={handleClick} disabled={redirecting}>
{redirecting ? 'Redirecting...' : 'Subscribe'}
</button>
)
}
ブラウザーは別のページに遷移するため、コールバックは実行されません。成功ページで (セッション ID を使って)支払いを確認するか、Webhook で確認してください。
モーダル / 埋め込みチェックアウト
useSubscribe を使用する(モーダルに推奨)
コールバックはフックのオプションです。subscribe() が受け取るのはチェックアウトオプションのみです。
'use client'
import { useSubscribe } from 'recur-tw'
import { useRouter } from 'next/navigation'
function SubscribeButton({ productId }: { productId: string }) {
const router = useRouter()
const { subscribe, isLoading, error, reset } = useSubscribe({
onPaymentComplete: (subscription) => {
// subscription: SubscriptionResult — id, status, productId, amount, currentPeriodEnd
router.push('/dashboard')
},
onPaymentFailed: (error) => {
console.error('Failed:', error.code, error.message)
return { action: 'retry' } // or 'close' or 'custom'
},
onPaymentCancel: () => console.log('User cancelled'),
})
return (
<>
<button
onClick={() => subscribe({ productId, customerEmail: '[email protected]' })}
disabled={isLoading}
>
{isLoading ? 'Processing...' : 'Subscribe'}
</button>
{error && <p className="error" onClick={reset}>{error.message}</p>}
</>
)
}
useSubscribe() は { subscribe, mutate, isLoading, error, reset } を返します。subscription フィールドは
ありません。結果を取得するには onPaymentComplete を使用してください。
useRecur().checkout を使用する(コールバックをインラインで指定)
'use client'
import { useRecur } from 'recur-tw'
function CheckoutButton({ productId }: { productId: string }) {
const { checkout, isCheckingOut } = useRecur()
const handleClick = () =>
checkout({
productId,
customerEmail: '[email protected]', // optional; collected in the form if omitted
customerName: 'John Doe',
externalCustomerId: 'user_123',
onPaymentComplete: (subscription) => console.log('Success!', subscription.id, subscription.status),
onPaymentFailed: (error) => ({ action: 'retry' }),
onPaymentCancel: () => console.log('User cancelled'),
})
return (
<button onClick={handleClick} disabled={isCheckingOut}>
{isCheckingOut ? 'Processing...' : 'Subscribe'}
</button>
)
}
モーダル / 埋め込み用のプロバイダー設定
// Modal (popup)
<RecurProvider
config={{
publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY!,
checkoutMode: 'modal',
}}
>
{children}
</RecurProvider>
// Embedded (inline form) — the container must exist in the DOM
<RecurProvider
config={{
publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY!,
checkoutMode: 'embedded',
containerElementId: 'recur-checkout-container',
}}
>
{children}
</RecurProvider>
function CheckoutPage() {
return (
<div>
<h1>Complete Your Purchase</h1>
{/* Recur renders the payment form here */}
<div id="recur-checkout-container" />
</div>
)
}
モーダル / 埋め込みチェックアウトには登録済みドメインが必要です。localhost ではホステッドチェックアウトを使用してください。
オプションの型
// redirectToCheckout()
interface RedirectToCheckoutOptions {
productId?: string // or productSlug — one is required
productSlug?: string
mode?: 'PAYMENT' | 'SUBSCRIPTION' | 'SETUP' // usually inferred from the product
successUrl: string
cancelUrl: string
customerEmail?: string
customerName?: string
externalCustomerId?: string
}
// checkout() / subscribe()
interface CheckoutOptions {
productId?: string
productSlug?: string
customerEmail?: string
customerName?: string
externalCustomerId?: string
successUrl?: string // used if 3-D Secure needs a redirect
cancelUrl?: string
// checkout() only — for subscribe() pass these to useSubscribe():
onPaymentComplete?: (subscription: SubscriptionResult) => void
onPaymentFailed?: (error: CheckoutError) => PaymentFailedAction | void
onPaymentCancel?: () => void
onSuccess?: (result: CheckoutResult) => void // session created (before payment)
onError?: (error: CheckoutError) => void
}
interface SubscriptionResult { // onPaymentComplete argument
id: string
status: string // 'ACTIVE', 'TRIALING', ...
productId: string
amount: number // whole TWD (499 = NT$499). NEVER divide by 100
billingPeriod: string
currentPeriodStart: string
currentPeriodEnd: string
trialEndsAt?: string
nextBillingDate?: string
}
これらのオプションに trialDays、quantity、metadata はありません。トライアルと価格は製品によって決まります(サーバー SDK の checkoutSessions.create() は metadata を受け取ります)。
製品の種類
4 種類すべてで同じ呼び出しを使用し、動作は製品によって決まります。
redirectToCheckout({ productSlug: 'pro-monthly', successUrl, cancelUrl }) // SUBSCRIPTION (recurring)
redirectToCheckout({ productSlug: 'ebook', successUrl, cancelUrl }) // ONE_TIME
redirectToCheckout({ productSlug: 'credits-100', successUrl, cancelUrl }) // CREDITS (prepaid wallet)
redirectToCheckout({ productSlug: 'support-us', successUrl, cancelUrl }) // DONATION (variable amount)
製品の一覧表示
import { useProducts } from 'recur-tw'
function PricingPage() {
const { data: products, isLoading, error } = useProducts({ type: 'SUBSCRIPTION' })
if (isLoading) return <div>Loading...</div>
if (error) return <div>{error.message}</div>
return (
<div className="pricing-grid">
{products?.map((product) => (
<PricingCard key={product.id} product={product} />
))}
</div>
)
}
Product のフィールド:id、name、slug、description、type、billingPeriod
('MONTHLY' | 'YEARLY' | ... | null)、price(TWD の整数)、currency、trialDays、
metadata、productFamily、displayOrder。SDK の型に priceFormatted はありません。次のように書式設定してください。
`NT$${product.price.toLocaleString()}`。
支払い失敗時の処理
支払いが拒否された場合、CheckoutError.code は 'PAYMENT_FAILED' です。ゲートウェイ独自のコードは
error.details.failure_code にあります(failure_message と can_retry も含まれます)。
CheckoutErrorDetails はユニオン型です。競合エラーの場合は配列、支払い失敗の場合はオブジェクトとなるため、
フィールドを読み取る前に型を絞り込んでください。
onPaymentFailed: (error) => {
const details = Array.isArray(error.details) ? undefined : error.details
switch (details?.failure_code) {
case 'CARD_DECLINED':
return { action: 'retry' }
case 'INSUFFICIENT_FUNDS':
return { action: 'custom', customTitle: '餘額不足', customMessage: '請使用其他付款方式' }
default:
return details?.can_retry ? { action: 'retry' } : { action: 'close' }
}
}
サーバーサイドのチェックアウト(ホステッド、React なし)
import { Recur } from 'recur-tw/server'
const recur = new Recur(process.env.RECUR_SECRET_KEY!)
const session = await recur.checkoutSessions.create({
productId: 'prod_xxx', // a CUID from list_products
successUrl: 'https://yourapp.com/success',
cancelUrl: 'https://yourapp.com/cancel',
customerEmail: '[email protected]',
metadata: { plan: 'pro' }, // optional, server-side only
})
// redirect the customer to session.url
または、プレーンな REST を使用します。
const response = await fetch('https://api.recur.tw/v1/checkout/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RECUR_SECRET_KEY}`, // X-Recur-Secret-Key also works
'Content-Type': 'application/json',
},
body: JSON.stringify({
productId: 'prod_xxx',
customerEmail: '[email protected]',
successUrl: 'https://yourapp.com/success',
cancelUrl: 'https://yourapp.com/cancel',
}),
})
// The session object is the response body itself (no outer `data` wrapper, snake_case keys)
const { url } = await response.json()
// Redirect the customer to `url`. (/v1/checkouts is the embedded-form endpoint, different shape.)
ベストプラクティス
- ホステッドチェックアウトをデフォルトにする — localhost を含め、どこでも動作します
- モーダル / 埋め込みではすべてのコールバックを処理する — onPaymentComplete、onPaymentFailed、onPaymentCancel
- ローディング状態を表示する —
isCheckingOut(useRecur、モーダル / 埋め込みのみ)/isLoading(useSubscribe)。redirectToCheckout()の場合は独自にフラグを管理してください - 顧客情報がすでにある場合は事前入力する — 任意です
- externalCustomerId を使用して、Recur の顧客をユーザーシステムに紐付ける
- まずサンドボックスでテストする —
pk_test_/sk_test_キー
関連スキル
/recur-quickstart- SDK の初期設定/recur-webhooks- 支払い通知の受信/recur-entitlements- サブスクリプションのアクセス権を確認