mcpField
R
recur-checkout

recur-checkout

github.com/recur-tw開発037 回インストール3 週間前に更新
リポジトリ

recur-checkout について

recur-checkoutは、recur-twを使ったRecurの決済フロー実装スキルです。ホスト型、モーダル型、埋め込み型のチェックアウトに加え、Reactとサーバー側の例、商品一覧、決済結果の処理を扱います。

主な機能

  1. 01ホスト型・モーダル型・埋め込み型チェックアウトの実装
  2. 02React hooksによるチェックアウトとサブスクリプション
  3. 03種類別の商品一覧
  4. 04決済完了・失敗・キャンセルの処理
  5. 05サーバー側でホスト型チェックアウトセッションを作成

活用シーン

  1. 01サブスクリプションや単発購入の決済を追加
  2. 02モーダルまたは埋め込み型の決済画面を提供
  3. 03商品選択ページや料金ページを構築
  4. 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.)

ベストプラクティス

  1. ホステッドチェックアウトをデフォルトにする — localhost を含め、どこでも動作します
  2. モーダル / 埋め込みではすべてのコールバックを処理する — onPaymentComplete、onPaymentFailed、onPaymentCancel
  3. ローディング状態を表示する — isCheckingOut(useRecur、モーダル / 埋め込みのみ)/ isLoading(useSubscribe)。redirectToCheckout() の場合は独自にフラグを管理してください
  4. 顧客情報がすでにある場合は事前入力する — 任意です
  5. externalCustomerId を使用して、Recur の顧客をユーザーシステムに紐付ける
  6. まずサンドボックスでテストする — pk_test_ / sk_test_ キー

関連スキル

  • /recur-quickstart - SDK の初期設定
  • /recur-webhooks - 支払い通知の受信
  • /recur-entitlements - サブスクリプションのアクセス権を確認

コメント