关于 recur-portal
用于实现 Recur Customer Portal 流程的技能,让订阅者自行管理订阅和账单,无需联系支持人员。内容涵盖服务端会话创建、Next.js 集成模式及直接调用 REST API。
主要功能
- 01在服务端创建门户会话
- 02按 ID 或邮箱识别客户
- 03集成 Next.js 路由和操作
- 04直接调用 REST API
- 05遵循身份验证和会话安全指南
适用场景
- 01添加自助账户门户入口
- 02让订阅者更新付款方式
- 03启用取消订阅或更改方案
- 04通过门户查看账单历史
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ recur-tw/ skills --skill recur-portal安装后在 Claude Code 中输入/recur-portal确认已加载。
详细介绍
Recur 客户门户集成
你正在协助实现 Recur 的客户门户,让订阅者无需联系客服即可自行管理订阅。
什么是客户门户?
客户门户是一个托管页面,客户可以在其中:
- 查看有效订阅和账单历史记录
- 更新付款方式
- 取消或重新激活订阅
- 切换套餐(升级/降级)
适用场景
| 场景 | 解决方案 |
|---|---|
| “添加账户管理页面” | 创建门户会话并重定向 |
| “让用户更新银行卡” | 由门户处理付款方式更新 |
| “用户需要取消订阅” | 门户提供自助取消功能 |
| “显示账单历史记录” | 门户显示发票和付款记录 |
快速入门:创建门户会话
门户会话需在服务端创建(需要 Secret Key)。
使用服务端 SDK
import { Recur } from 'recur-tw/server'
const recur = new Recur(process.env.RECUR_SECRET_KEY!)
// Create portal session - identify customer by email, ID, or externalId
const session = await recur.portal.sessions.create({
email: '[email protected]', // or customer: 'cus_xxx' or externalId: 'user_123'
returnUrl: 'https://yourapp.com/account',
})
// Redirect customer to portal
redirect(session.url)
客户识别
可通过以下任一方法识别客户(按优先级排序):
// By Recur customer ID (highest priority)
await recur.portal.sessions.create({
customer: 'cus_xxx',
returnUrl: 'https://yourapp.com/account',
})
// By your system's user ID
await recur.portal.sessions.create({
externalId: 'user_123',
returnUrl: 'https://yourapp.com/account',
})
// By email (lowest priority)
await recur.portal.sessions.create({
email: '[email protected]',
returnUrl: 'https://yourapp.com/account',
})
Next.js 实现
API 路由(App Router)
// app/api/portal/route.ts
import { Recur } from 'recur-tw/server'
import { auth } from '@/lib/auth' // Your auth solution
import { NextResponse } from 'next/server'
const recur = new Recur(process.env.RECUR_SECRET_KEY!)
export async function POST() {
const session = await auth()
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const portalSession = await recur.portal.sessions.create({
email: session.user.email,
returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
})
return NextResponse.json({ url: portalSession.url })
} catch (error) {
console.error('Portal session error:', error)
return NextResponse.json(
{ error: 'Failed to create portal session' },
{ status: 500 }
)
}
}
服务端操作
// app/actions/portal.ts
'use server'
import { Recur } from 'recur-tw/server'
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
const recur = new Recur(process.env.RECUR_SECRET_KEY!)
export async function openPortal() {
const session = await auth()
if (!session?.user?.email) {
throw new Error('Unauthorized')
}
const portalSession = await recur.portal.sessions.create({
email: session.user.email,
returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
})
redirect(portalSession.url)
}
门户按钮组件
// components/portal-button.tsx
'use client'
import { useState } from 'react'
export function PortalButton() {
const [isLoading, setIsLoading] = useState(false)
const handleClick = async () => {
setIsLoading(true)
try {
const response = await fetch('/api/portal', { method: 'POST' })
const { url, error } = await response.json()
if (error) throw new Error(error)
window.location.href = url
} catch (error) {
console.error('Failed to open portal:', error)
alert('無法開啟帳戶管理頁面,請稍後再試')
} finally {
setIsLoading(false)
}
}
return (
<button onClick={handleClick} disabled={isLoading}>
{isLoading ? '載入中...' : '管理訂閱'}
</button>
)
}
使用服务端操作和按钮
// components/portal-button-action.tsx
'use client'
import { openPortal } from '@/app/actions/portal'
export function PortalButton() {
return (
<form action={openPortal}>
<button type="submit">管理訂閱</button>
</form>
)
}
门户会话响应
interface PortalSession {
id: string // Session ID (e.g., 'portal_sess_xxx')
object: 'portal.session'
url: string // URL to redirect customer to
customer: string // Customer ID
returnUrl: string // URL to return after portal exit
status: 'active' | 'expired'
expiresAt: string // ISO 8601 (sessions last 1 hour)
accessedAt: string | null
createdAt: string
}
直接使用 REST API
如果不使用 SDK:
const response = await fetch('https://api.recur.tw/v1/portal/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
return_url: 'https://yourapp.com/account',
}),
})
const { url } = await response.json()
// Redirect to url
常见模式
带有门户链接的账户页面
// app/account/page.tsx
import { auth } from '@/lib/auth'
import { PortalButton } from '@/components/portal-button'
export default async function AccountPage() {
const session = await auth()
return (
<div>
<h1>帳戶設定</h1>
<p>Email: {session?.user?.email}</p>
<section>
<h2>訂閱管理</h2>
<p>管理您的訂閱、更新付款方式、查看帳單記錄</p>
<PortalButton />
</section>
</div>
)
}
有条件地开放门户访问
// Only show portal button if user has subscription
function SubscriptionSection({ hasSubscription }: { hasSubscription: boolean }) {
if (!hasSubscription) {
return (
<div>
<p>您目前沒有訂閱</p>
<a href="/pricing">查看方案</a>
</div>
)
}
return (
<div>
<p>您目前的訂閱:Pro 方案</p>
<PortalButton />
</div>
)
}
门户配置
在 Recur Dashboard → Settings → Customer Portal 中配置门户行为:
- 默认返回 URL:离开门户后要重定向到的位置
- 允许的操作:启用/禁用取消订阅、更新付款方式和切换套餐
- 品牌元素:自定义徽标和颜色
安全说明
- 仅限服务端:门户会话需要 Secret Key(sk_xxx)
- 有效期较短:会话会在 1 小时后过期
- 过期前可重复使用:该 URL 在这一小时内持续有效,因此应按需创建,且绝不要记录日志或通过电子邮件发送
- 验证用户身份:创建门户会话前,务必验证用户身份
错误处理
import { RecurAPIError } from 'recur-tw/server'
let session
try {
session = await recur.portal.sessions.create({
email: userEmail,
returnUrl: returnUrl,
})
} catch (error) {
if (error instanceof RecurAPIError && error.code === 'not_found') {
// No Recur customer matches this email / externalId / customer id,
// usually because they have not bought anything yet
redirect('/pricing')
}
// code 'bad_request': neither returnUrl nor a default return URL
// (Dashboard → Settings → Customer Portal) is set
throw error
}
redirect(session.url)
将 redirect() 放在 try 外部:在 Next.js 中,它通过抛出异常来工作,外围的 catch 会拦截它。
相关技能
/recur-quickstart- 初始 SDK 设置/recur-checkout- 实现购买流程/recur-entitlements- 检查订阅访问权限
评论
支持 Markdown