关于 recur-checkout
recur-checkout 是使用 recur-tw 实现 Recur 支付结账流程的技能,涵盖托管式、弹窗式和嵌入式结账。内容包括 React 与服务端示例、商品列表及支付结果处理。
主要功能
- 01实现托管式、弹窗式和嵌入式结账
- 02使用 React 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(),并在 provider 中设置 checkoutMode: 'modal' | 无需离开页面即可快速购买 |
| 嵌入式 | 使用相同的 hooks,在 provider 中设置 checkoutMode: 'embedded' + containerElementId | 自定义结账页面 |
checkoutMode 在 <RecurProvider config> 上设置,而不是在调用时设置。provider 的默认值是
'embedded',如果没有 containerElementId 就会报错——需要弹出窗口时,请设置 checkoutMode: 'modal'。任何调用都没有
mode: 'hosted' | 'modal' 选项。
托管式结账(推荐)
只有 checkout() 会设置 isCheckingOut;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(模态框推荐)
回调是 hook 的选项;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>
)
}
模态框 / 嵌入式模式的 provider 设置
// 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)。
产品类型
所有四种类型都使用相同的调用;具体行为由产品决定:
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- 检查订阅权限