关于 freemius-customer-portal
用于在应用中接入 Freemius 购后客户门户的 Skill,面向开发者提供服务端驱动的接口与 React 组件方案,涵盖订阅、账单、取消和发票。
主要功能
- 01单一服务端门户接口
- 02查看订阅并升级许可证
- 03取消续订并提供留存优惠券
- 04更新账单信息及获取发票 PDF
适用场景
- 01添加自助订阅管理
- 02取消续订前提供优惠券
- 03允许客户更新账单信息
- 04展示付款记录并下载发票
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ freemius/ freemius-ai --skill freemius-customer-portal安装后在 Claude Code 中输入/freemius-customer-portal确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/billing.mdcancellation-and-coupon.mdheadless-endpoint.mdpayments-and-invoices.mdsubscription-and-upgrade.md详细介绍
Freemius 客户门户
为已登录用户提供一个页面,管理购买后的所有事项: 订阅、升级、取消(附带挽留优惠)、账单信息 和发票 — 无需在应用中重新实现任何账单逻辑。
来源:React Starter Kit Customer Portal 组件 https://freemius.com/help/documentation/saas-sdk/react-starter/components/#customer-portal-component
框架说明。 本技能中的路由示例采用 Hono 编写,但 SDK 的门户处理器适用于支持 Web 标准
Request/Response路由的任何框架 — 请根据应用实际使用的 HTTP 框架调整这个轻量路由。React Starter KitCustomerPortal组件面向 React 19+、shadcn UI 和 Tailwind(通过npx shadcn@latest add https://shadcn.freemius.com/all.json安装);如果应用 不使用 shadcn/React,请将其用作参考实现。
沙盒。 验证期间,请确保处理器的
isSandbox绑定到专用的FREEMIUS_SANDBOX环境变量(绝不要使用NODE_ENV),使其与 结账的沙盒模式相匹配 — 沙盒是一种使用测试信用卡的结账模式, 由freemius-core持有。
黄金规则(不得违反)
- 门户采用无头设计,由服务端驱动。 SDK 提供了一个
PortalRequestProcessor:后端只需公开一个端点 (GET|POST /api/portal),SDK 就会通过签名的单次操作 URL 处理数据检索_和_所有操作。组件绝不会直接调用 Freemius。 - 操作使用令牌签名。 门户数据会携带预签名的操作 URL
(取消、优惠券、账单更新、发票)。组件只需访问这些
URL;SDK 会在服务端验证令牌。你只需提供
已通过身份验证的用户身份。(升级是个例外 — 它会通过服务端构建的许可证升级结账流程(
setLicenseUpgradeByKey)进行, 避免重复订阅 / 续订许可证时出现 500 错误。) - 身份验证由你负责;其他一切都交给 SDK。 处理器的
getUser会从你的会话中解析 Freemius 用户(例如通过电子邮件查找)。所有 Freemius 凭据都保留在服务端。 - 取消订阅前应先尝试挽留。 如果有续订取消优惠券, 请在完成取消前先提供该优惠。
唯一的端点
// src/server/services/freemius/portal.ts
import { freemius, IS_SANDBOX } from './client';
import { env } from '../../env';
const PORTAL_ENDPOINT = `${env.publicAppUrl}/api/portal`;
export async function processPortalRequest(
request: Request,
email: string
): Promise<Response> {
const processor = freemius.customerPortal.request.createProcessor({
portalEndpoint: PORTAL_ENDPOINT, // must match the public URL the browser hits
isSandbox: IS_SANDBOX,
getUser: async () => {
const fsUser = await freemius.api.user.retrieveByEmail(email);
return fsUser?.id ? { id: String(fsUser.id), email } : { email };
},
});
return processor(request); // dispatches by ?action= and verifies tokens
}
// src/server/routes/portal.ts — route stays thin: auth, then delegate.
portalRoute.on(['GET', 'POST'], '/', requireUser, async (c) =>
processPortalRequest(c.req.raw, c.get('user').email)
);
处理器会根据 ?action= 查询参数进行分发:
| 方法 | ?action= | 结果 |
|---|---|---|
| GET | portal_data | 订阅 + 账单 + 付款 + 签名 URL |
| POST | billing | 更新账单信息 |
| POST | subscription_cancellation | 取消续订(+ 可选反馈) |
| POST | subscription_cancellation_coupon | 应用挽留优惠券 |
| GET | invoice | 流式传输发票 PDF |
参见 references/headless-endpoint.md。
组件
将 CustomerPortal 移植为可编辑的 shadcn/ui 风格 React 源码。它会获取一次
?action=portal_data,并渲染四个部分,每个部分都由该数据中包含的签名 URL 驱动:
| 部分 | 用户操作 | 参考 |
|---|---|---|
| 订阅 | 查看当前方案;通过服务器构建的许可证升级结账(setLicenseUpgradeByKey)进行升级——这是稳健的方案(可避免重复订阅 / 续订许可证时报 500 错误) | references/subscription-and-upgrade.md |
| 取消 | 取消续订——免责声明 → 留存优惠券 → 反馈 | references/cancellation-and-coupon.md |
| 账单 | 编辑公司名称 / 税号 / 地址 | references/billing.md |
| 付款与发票 | 查看付款历史,下载发票 PDF | references/payments-and-invoices.md |
快速开始(前端)
import { CustomerPortal } from './react-starter/components/customer-portal';
import { PORTAL_ENDPOINT, portalFetch } from './api';
// portalFetch attaches the auth header; the component does the rest.
<CustomerPortal endpoint={PORTAL_ENDPOINT} fetcher={portalFetch} />;
// Loading portal data:
const res = await fetcher(`${endpoint}?action=portal_data`);
const data = (await res.json()) as PortalData;
// data.subscriptions.{primary,active,past}, data.billing, data.payments,
// data.cancellationCoupons — each carries signed action URLs.
基本概念
Frontend <CustomerPortal> Backend (one endpoint) Freemius
GET /api/portal?action=portal_data ─▶ processPortalRequest()
{ subscriptions, billing, payments, ◀─ freemius.customerPortal.request
+ signed action URLs } .createProcessor() → getUser()
POST <signed cancelRenewalUrl> ───────▶ processor verifies token ──────────▶ cancel renewal
POST <signed couponUrl> ──────────────▶ processor verifies token ──────────▶ apply coupon
POST <signed billing updateUrl> ──────▶ processor verifies token ──────────▶ update billing
GET <signed invoiceUrl> ─────────────▶ processor verifies token ──────────▶ invoice PDF
GET /api/checkout/upgrade ───────────▶ license.retrieve + checkout.create()
{ link } ◀──────────────────────────── .setLicenseUpgradeByKey(secret_key) ─▶ plan-change
.getLink() — browser navigates ─────▶ checkout
⚠️ 升级路径是上文介绍的服务器构建的许可证升级结账。并且不要为此使用
checkout.create({ licenseId }):对于正在续订的许可证,此选项会返回 500 错误(SDK 在内部调用了一个付款方式更新端点,该端点会将expiration: null的许可证判定为“永久许可证”并拒绝请求)。在服务端获取许可证,并改用setLicenseUpgradeByKey(license.secret_key)。请参阅 references/subscription-and-upgrade.md。
Dashboard 配置(交由相关人员处理)
- Product → Coupons → 创建续订取消(留存)优惠券,以便填充
cancellationCoupons,让向导能够提供该优惠券。 - 负责保持本地权益同步的 webhook 和重定向配置由
freemius-core管理;升级 / 取消操作会以 webhook 事件的形式到达那里 (license.plan.changed、license.cancelled,……)。
操作后同步(更改如何传递到应用)
Portal 操作会先在 Freemius 中更改状态;本地
user_fs_entitlement 镜像会通过以下两种方式同步:
- 立即在 Portal UI 中同步——任何操作后重新获取
?action=portal_data(它会读取 Freemius 实时数据)。 - 同步到权益镜像中——通过
freemius-core的 webhooks (升级时为license.plan.changed,取消时为license.cancelled/ 到期事件)。⚠️ 在 Dashboard 中设置或更改 webhook URL 后,请等待约 5 分钟让其生效——URL 激活前触发的事件永远不会送达(不会补发)。请参阅freemius-core→references/webhooks.md(有关事件遗漏的情况,请参阅freemius-troubleshooting)。