@polar-sh/nuxt 适配器全解析:在 Nuxt 应用中集成 Polar 支付、Checkout 与 Webhook
2026/9/16 19:04:12 网站建设 项目流程

@polar-sh/nuxt 适配器全解析:在 Nuxt 应用中集成 Polar 支付、Checkout 与 Webhook

【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar

@polar-sh/nuxt 是 Polar 官方为 Nuxt 3 提供的服务端适配器模块,将 Polar 的托管结账(Checkout)、客户门户(Customer Portal)与 Webhook 验签能力以三个即插即用的 Server Handler 形式封装进 Nuxt 应用。本文以仓库内 CHANGELOG.md 的版本演进为主线,结合 README.md 与模块源码、测试用例,完整讲解安装配置、Checkout Query 参数、Webhook 细粒度事件处理器与权益(Entitlement)机制,读完即可在 Nuxt 3 应用中落地一套完整的 Polar 收款闭环。

模块定位与整体结构

@polar-sh/nuxt 当前版本为 0.5.8,定位是"Polar 的 Nuxt 集成适配器"(见 package.json 的 description),它不是一个业务组件库,而是一组运行在 Nuxt 服务端(Nitro/h3)的路由处理器工厂。模块源代码结构如下:

clients/adapters/nuxt/ ├── src/ │ ├── module.ts # Nuxt 模块定义(configKey: polar) │ └── runtime/server/ │ ├── checkoutHandler.ts # Checkout 处理器 │ ├── customerPortalHandler.ts # Customer Portal 处理器 │ ├── webhookHandler.ts # Webhook 验签与分发 │ └── index.ts # 统一导出 ├── playground/ # 本地开发调试用 Nuxt 应用 ├── test/ # vitest 单元测试 ├── CHANGELOG.md # 版本演进记录 └── README.md # 使用文档

从 module.ts 可以看出,模块通过defineNuxtModule注册,configKeypolar(即nuxt.config.ts中的polar: {}配置块),核心动作是调用addServerImportsDirruntime/server目录下的CheckoutCustomerPortalWebhooks三个工厂函数注册为 Nuxt 服务端自动导入,因此业务代码中无需手动 import。

在底层,三个处理器都基于 Polar TypeScript SDK(源码中引入的是@polar-sh/sdk/2026-04这个按 API 版本划分的命名空间,通过createPolarCore({ accessToken, environment })创建 SDK 客户端)与 h3 的事件对象(H3Event)协作。

安装与模块注册

按 README.md 与 package.json 的记录,安装要求 Node.js >= 22,使用任意包管理器安装即可:

pnpm add @polar-sh/nuxt

然后在nuxt.config.ts中注册模块:

export default defineNuxtConfig({ modules: ['@polar-sh/nuxt'], })

值得注意的一点是:仓库内的 playground/nuxt.config.ts 展示了推荐的运行时配置方式——把 Polar 的敏感凭据放进runtimeConfig.private,从而避免在业务代码里硬编码密钥:

export default defineNuxtConfig({ modules: ['../src/module'], polar: {}, compatibilityDate: '2025-02-25', runtimeConfig: { private: { polarAccessToken: '', polarServer: '', polarCheckoutSuccessUrl: '', polarWebhookSecret: '', }, }, })

这些私有运行时配置键(polarAccessTokenpolarServerpolarCheckoutSuccessUrlpolarWebhookSecret)会在后续三个 Handler 的示例代码中通过useRuntimeConfig()读取,建议在部署时用环境变量注入。

Checkout:一行代码接入 Polar 托管结账页

Polar 的 Checkout 是托管式的:你的服务端只需把客户重定向到 Polar 生成的结账 URL。@polar-sh/nuxt 的Checkout工厂负责创建结账会话并完成重定向(sendRedirect),参考 README 的最小实现:

// server/routes/api/checkout.post.ts export default defineEventHandler((event) => { const { private: { polarAccessToken, polarCheckoutSuccessUrl, polarServer }, } = useRuntimeConfig() const checkoutHandler = Checkout({ accessToken: polarAccessToken, successUrl: polarCheckoutSuccessUrl, returnUrl: 'https://myapp.com', // 可选:结账页显示"返回"按钮 environment: polarServer as 'sandbox' | 'production', theme: 'dark', // 强制深色主题;不传则跟随系统主题 }) return checkoutHandler(event) })

CheckoutConfig 配置项

对照 checkoutHandler.ts 中导出的CheckoutConfig类型,可用的配置如下:

配置项类型说明
accessTokenstringPolar 组织访问令牌,必填,用于创建结账会话
successUrlstring支付成功后的跳转地址;默认会追加checkout_id参数
returnUrlstring可选,在结账页渲染"返回"按钮的回跳地址(0.3.12 版本加入)
includeCheckoutIdboolean是否在successUrl中追加{CHECKOUT_ID}占位符,默认true
environment'sandbox' \| 'production'Polar 环境,对应 SDK 的Environment
theme'light' \| 'dark'强制结账页主题(0.3.3 版本加入),省略则跟随系统偏好

底层行为细节

从源码实现(checkoutHandler.ts)可以看到几个关键行为:

  • successUrl占位符:当includeCheckoutId(默认 true)时,处理器会把{CHECKOUT_ID}占位符写入successUrl的查询参数,SDK 创建会话后会用真实的结账 ID 替换,方便你在成功页读取checkout_id完成后续订单同步。
  • discount_iddiscount_code的优先级discount_id在创建会话时直接传给 API;而discount_code是在会话创建成功之后,通过clientUpdateCheckouts(result.client_secret, { discount_code })二次调用更新进去的。源码中的if (discountCode && !discountId)保证了"两者都传时以discount_id为准"(README 明确标注了这一优先级规则)。
  • 主题注入theme通过重定向 URL 的theme查询参数传给 Polar 结账页,因此测试用例中能看到最终重定向地址形如https://polar.sh/checkout/123?theme=dark
  • 错误处理:任何创建失败都会被包装成 h3 的 500 错误(createError({ statusCode: 500, statusMessage: error.message })),避免把 Polar 内部错误直接暴露给前端。

Checkout Query 参数全解析

Polar 结账会话的所有输入都通过请求当前路由的 Query 参数传递(README 称之为 Query Params)。README 只列举了 7 个常用参数,但源码中的 zod 校验模式(checkoutHandler.ts)实际支持完整参数集,下面给出全表:

参数必填示例说明
products?products=123商品 ID,可用逗号分隔传多个商品(0.3.0 起替代productId/productPriceId
customer_id?customer_id=xxx关联已有 Polar 客户
external_customer_id?external_customer_id=xxx你自己的系统中的客户 ID
customer_email?customer_email=janedoe@gmail.com预填客户邮箱
customer_name?customer_name=Jane预填客户姓名
customer_billing_addressURL 编码的 JSON账单地址对象(源码中用JSON.parse解析)
customer_tax_id?customer_tax_id=TAX1客户税号
customer_ip_address?customer_ip_address=10.0.0.1客户 IP
customer_metadataURL 编码的 JSON客户自定义元数据(JSON.parse解析)
allow_discount_codes?allow_discount_codes=true是否允许该结账会话使用折扣码(字符串"true"才会被解析为布尔真)
discount_id?discount_id=disc_1预选折扣,优先于discount_code
discount_code?discount_code=SAVE20预填折扣码,需在 Polar 后台开启折扣码功能
metadataURL 编码的 JSON结账会话元数据(JSON.parse解析)
seats?seats=5按席位计费的席位数量(parseInt解析)

几点使用提醒:

  • products是唯一必填参数,且支持逗号分隔多商品——这正是 0.3.0 版本的破坏性变更(见下文版本演进小节),旧的productIdproductPriceId已被移除。
  • 所有 JSON 类型参数(customer_billing_addresscustomer_metadatametadata)必须经过 URL 编码,处理器内部会调用JSON.parse还原为对象。
  • allow_discount_codes只接受字面量"true"(转小写后比较),其余值一律视为false

这些行为在 checkoutHandler.test.ts 中有对应的回归测试覆盖,例如"?seats=5透传为seats: 5"、"URL 转义保留"、"JSON 参数解析"、"discount_code触发二次更新接口"等。

Customer Portal:让客户自助管理订单与订阅

客户门户让已购客户自行查看订单和订阅状态。@polar-sh/nuxt 的CustomerPortal工厂接收一个getCustomerId回调,由你在服务端解析出当前登录客户,然后由它创建 Polar 客户会话并重定向:

// server/routes/api/portal.get.ts export default defineEventHandler((event) => { const { private: { polarAccessToken, polarServer }, } = useRuntimeConfig() const customerPortalHandler = CustomerPortal({ accessToken: polarAccessToken, environment: polarServer as 'sandbox' | 'production', getCustomerId: (event) => { return Promise.resolve('9d89909b-216d-475e-8005-053dba7cff07') }, returnUrl: 'https://myapp.com', // 可选:门户内"返回"按钮的地址 }) return customerPortalHandler(event) })

对照 customerPortalHandler.ts,CustomerPortalConfig包含四个字段:

配置项类型说明
accessTokenstringPolar 组织访问令牌
environment'sandbox' \| 'production'Polar 环境
getCustomerId(event: H3Event) => Promise<string>从请求中解析当前客户 ID 的异步回调
returnUrlstring可选,门户页面"返回"按钮回跳地址

源码中的关键逻辑(customerPortalHandler.ts):

  • await getCustomerId(event)拿到客户 ID;如果回调返回空值,直接抛 400 错误(customerId not defined),并在服务端打印错误日志。
  • 拿到 ID 后调用 SDK 的createCustomerSessions创建客户会话,returnUrl会经过decodeURI处理后再传给 Polar,最后sendRedirectresult.customer_portal_url
  • 会话创建失败同样包装为 h3 500 错误。

Webhooks:签名验证 + 细粒度事件分发

Polar 通过 Webhook 把订单、订阅、权益等事件推送到你的服务器。@polar-sh/nuxt 的Webhooks处理器封装了完整的验签流程——它从请求头读取webhook-idwebhook-timestampwebhook-signature,读取原始请求体(readRawBody),然后交给 SDK 的webhooks.validateEvent验证签名:

// server/routes/webhook/polar.post.ts export default defineEventHandler((event) => { const { private: { polarWebhookSecret }, } = useRuntimeConfig() const webhooksHandler = Webhooks({ webhookSecret: polarWebhookSecret, onPayload: async (payload: any) => { // 处理所有事件 // 无需返回确认响应 }, }) return webhooksHandler(event) })

验签与错误响应语义

对照 webhookHandler.ts,验签失败时的响应有明确的 HTTP 语义:

异常类型响应说明
PolarWebhookVerificationError403{ received: false }签名验证失败,拒绝该请求
PolarWebhookUnknownTypeError事件类型为 null 时 400,否则 200未知事件类型按"已接收"或"非法"处理
其他PolarWebhookError400{ received: false }通用 Webhook 错误
验签通过200{ received: true }正常处理并确认

这里特别值得注意 0.5.4 版本的修复:"Fix webhook signature verification failing on parsed + re-serialized webhook bodies"。这解释了为什么处理器必须用readRawBody读取原始请求体再交给validateEvent——如果先用 JSON.parse 再序列化,字节内容会与 Polar 签名时使用的原始字节不一致,导致验签失败。这一点对自行实现验签的读者同样是重要提示。

细粒度 Payload Handlers

Webhooks配置除了通用onPayload外,还支持按事件类型注册独立处理器(README 与 webhooks.ts 中定义的完整列表):

  • CheckoutonCheckoutCreatedonCheckoutUpdatedonCheckoutExpired
  • OrderonOrderCreatedonOrderUpdatedonOrderPaidonOrderRefunded
  • RefundonRefundCreatedonRefundUpdated(0.3.8 / 0.3.9 版本加入)
  • SubscriptiononSubscriptionCreatedonSubscriptionUpdatedonSubscriptionActiveonSubscriptionCanceledonSubscriptionCycledonSubscriptionPastDueonSubscriptionPausedonSubscriptionResumedonSubscriptionRevokedonSubscriptionUncanceled
  • ProductonProductCreatedonProductUpdated
  • OrganizationonOrganizationUpdated
  • BenefitonBenefitCreatedonBenefitUpdated
  • Benefit GrantonBenefitGrantCreatedonBenefitGrantUpdatedonBenefitGrantRevokedonBenefitGrantCycled
  • CustomeronCustomerCreatedonCustomerUpdatedonCustomerDeletedonCustomerStateChanged(0.2.2 版本加入客户状态支持)
  • Customer SeatonCustomerSeatAssignedonCustomerSeatClaimedonCustomerSeatRevoked
  • DiscountonDiscountCreatedonDiscountUpdatedonDiscountDeleted
  • MemberonMemberCreatedonMemberUpdatedonMemberDeleted

这些处理器定义在 @polar-sh/nuxt 的底层依赖 @polar-sh/adapter-utils 中,handleWebhookPayloadswitch (payload.type)把事件精确分发到对应回调,并用Promise.all并发执行所有命中的处理器(通用onPayload总会执行,事件专属处理器按需执行),最后统一await全部完成——这正是 0.1.2 版本"Await webhook handlers"变更带来的语义:处理器函数返回的 Promise 会被完整等待,确保异步副作用(如写库)完成后才返回响应。

从 Webhook 到权益(Entitlements)

@polar-sh/nuxt 的Webhooks配置还支持传入entitlements(权益分发器),用于在benefit_grant.created/benefit_grant.revoked事件发生时自动执行授权/撤销逻辑。机制位于 adapter-utils 的 entitlement 实现:

import { Entitlements, EntitlementStrategy } from '@polar-sh/adapter-utils' const proStrategy = new EntitlementStrategy() .grant(async ({ customer, properties }) => { // 授予权益:按 properties 为 customer 开通功能 }) .revoke(async ({ customer }) => { // 撤销权益 }) Entitlements.use('pro-plan', proStrategy)

其设计要点:

  • EntitlementStrategy以链式方式注册grant/revoke回调,通过handler(slug)生成一个EntitlementHandler
  • Entitlements.use(slug, strategy)把策略挂载到全局静态handlers数组,最终传入Webhooksentitlements配置。
  • 匹配规则:回调中拿payload.data.benefit.description === slug来判定该事件是否属于该权益策略,事件携带的customerproperties会注入到回调上下文EntitlementContext中。

权益相关能力从 0.1.11(导出 Entitlement 类)到 0.1.13(导出权益工具)逐步完善,是适配器"收款闭环"之外的重要一环——订单支付后自动授权益、退款或订阅取消后自动撤销权益。

版本演进:从 CHANGELOG 看模块能力的时间线

CHANGELOG 完整记录了 @polar-sh/nuxt 从 0.1.x 到 0.5.8 的演进,按功能域归纳如下:

0.1.x:能力奠基期

  • 0.1.1 加入细粒度 Webhook 处理器(granular webhook handlers);
  • 0.1.2 修复 Webhook 处理器未被 await的问题;
  • 0.1.9 增加productPriceId参数能力;0.1.10 保证结账时必须传 price 或 product;
  • 0.1.11 导出 Entitlement 类;0.1.12 落地 entitlements 实现并初始化 Nuxt 接入;0.1.13 导出权益工具;
  • 0.1.14 导出类型;0.1.15 修复 URI 解码(decode the URI properly)。

0.2.x:模块化与状态支持

  • 0.2.1 正式初始化 Nuxt 模块包结构;0.2.2 加入客户状态(customer state)支持;0.2.4 改善错误信息;0.2.5 加入新订单 Webhook 支持。

0.3.x:Checkout 能力大版本

  • 0.3.0(破坏性变更):Checkout 端点不再支持productId/productPriceId传商品,统一改用可重复的products参数,一次结账可传多个商品;
  • 0.3.1 修复products参数传递问题;0.3.3 为 Checkout 配置加入主题支持(theme);0.3.4 修复 SDK 误解析 Zod v4 的问题;0.3.5 修复导入路径;
  • 0.3.8 / 0.3.9 两连发加入退款 Webhook(refund webhooks);
  • 0.3.12 加入returnUrl支持(结账页/门户"返回"按钮)。

0.4.x:SDK 升级

  • 0.4.0 将 SDK 更新到 0.40.2,adapter-utils 同步升至 0.3.0。

0.5.x:稳定性与 SDK 同步

  • 0.5.0 升级 Polar SDK;0.5.1 统一升级依赖;0.5.2 / 0.5.3 持续跟随 SDK 发布;0.5.4 修复"解析后重新序列化的 Webhook 请求体导致验签失败"——即前文强调的readRawBody原始字节验签;0.5.5 ~ 0.5.7 密集跟随 SDK 更新(0.46.0 → 0.47.0 区间,其间 adapter-utils 同步升级);0.5.8 将 adapter-utils 更新到 0.4.7。

当前仓库中 package.json 记录的依赖为@polar-sh/sdk@1.0.0-alpha.20@polar-sh/adapter-utils(workspace 版本,即仓库内 adapter-utils),CHANGELOG 中 0.3.x ~ 0.5.x 的 SDK 版本号属于历史快照,二者并不矛盾——这反映的是该适配器长期保持"紧跟 SDK 发布节奏"的维护策略。

测试与验证

适配器配有 vitest 单元测试(见 test/checkoutHandler.test.ts),通过 mock SDK 与sendRedirectCheckout处理器做行为级断言,覆盖了:

  • URL 转义保留%2541%2500%7Bx%7D等在successUrl/returnUrl中不被二次破坏,{CHECKOUT_ID}占位符可正常替换;
  • seats 透传?seats=5/1/0的透传,以及非数字输入按NaN透传(与 nextjs 适配器保持一致性);
  • 多参数组合products逗号分隔、客户字段、allow_discount_codesdiscount_id的组合透传;
  • JSON 参数解析customer_billing_addresscustomer_metadatametadataJSON.parse行为;
  • 折扣流程discount_code触发clientUpdateCheckouts二次调用、discount_id存在时不再调用、折扣应用失败时不重定向;
  • 主题注入theme=dark追加到重定向 URL;
  • 错误处理:创建结账失败时抛 h3 500 错误。

运行测试使用:

pnpm test # 在 clients/adapters/nuxt 目录下执行

小结

@polar-sh/nuxt 把 Polar 的结账、客户门户与 Webhook 三个核心能力压缩为三个配置型工厂函数,开发者只需在 Nuxt 服务端路由中调用CheckoutCustomerPortalWebhooks即可完成接入。从 CHANGELOG 的演进脉络看,模块的能力增长点集中在:多商品结账(0.3.0products)、结账主题与返回链接(0.3.3 / 0.3.12)、退款事件与客户状态(0.3.8 / 0.2.2)、原始字节验签修复(0.5.4)以及贯穿始终的 SDK 同步升级——这为理解"如何正确使用该适配器"以及"自行接入 Polar 时应关注哪些细节"提供了清晰的路线图。

【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询