Polar

使用 Polar 实现更佳的支付和结账验证插件

Polar 是一个以开发者为中心的支付基础设施。开箱即用,提供了许多面向开发者的支付、结账等集成。此插件帮助你将 Polar 与 Better Auth 集成,实现无缝的身份验证和支付流程。

本插件由 Polar 团队维护。如遇错误、问题或功能请求, 请访问 Polar GitHub 仓库

功能

  • 结账集成
  • 客户门户
  • 注册时自动创建客户
  • 事件摄取与客户计量器,支持灵活的基于用量的计费
  • 使用签名验证安全处理 Polar Webhooks
  • 引用系统以将购买关联至组织

安装

pnpm add better-auth @polar-sh/better-auth @polar-sh/sdk

准备工作

进入你的 Polar 组织设置,创建一个组织访问令牌,并添加到你的环境变量中。

# .env
POLAR_ACCESS_TOKEN=...

配置 BetterAuth 服务端

Polar 插件包含若干附加插件,为你的堆栈增加功能。

  • Checkout - 实现无缝的结账集成
  • Portal - 让客户管理订单、订阅及授予的权益
  • Usage - 简单扩展,用于列出客户计量器及摄取事件,支持基于用量计费
  • Webhooks - 监听相关的 Polar webhook 事件
import { betterAuth } from "better-auth";
import { polar, checkout, portal, usage, webhooks } from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";

const polarClient = new Polar({
    accessToken: process.env.POLAR_ACCESS_TOKEN,
    // 如果使用 Polar 沙盒环境,则使用 'sandbox'
    // 记住,访问令牌、产品等在不同环境间完全分隔。
    // 生产环境获取的访问令牌不能在沙盒环境使用。
    server: 'sandbox'
});

const auth = betterAuth({
    // ... Better Auth 配置
    plugins: [
        polar({
            client: polarClient,
            createCustomerOnSignUp: true,
            use: [
                checkout({
                    products: [
                        {
                            productId: "123-456-789", // 来自 Polar 仪表盘的产品 ID
                            slug: "pro" // 自定义 slug 以便在结账 URL 中引用,例如 /checkout/pro
                        }
                    ],
                    successUrl: "/success?checkout_id={CHECKOUT_ID}",
                    authenticatedUsersOnly: true
                }),
                portal(),
                usage(),
                webhooks({
                    secret: process.env.POLAR_WEBHOOK_SECRET,
                    onCustomerStateChanged: (payload) => // 当客户相关信息变更时触发
                    onOrderPaid: (payload) => // 当订单付款成功时触发(购买、订阅续费等)
                    ...  // 超过 25 个细粒度 webhook 处理器
                    onPayload: (payload) => // 捕获所有事件的通用处理器
                })
            ],
        })
    ]
});

配置 BetterAuth 客户端

你将使用 BetterAuth 客户端与 Polar 功能交互。

import { createAuthClient } from "better-auth/react";
import { polarClient } from "@polar-sh/better-auth/client";

// 就这么简单
// 所有 Polar 插件应绑定到服务端的 BetterAuth 配置上
export const authClient = createAuthClient({
  plugins: [polarClient()],
});

配置选项

import { betterAuth } from "better-auth";
import {
  polar,
  checkout,
  portal,
  usage,
  webhooks,
} from "@polar-sh/better-auth";
import { Polar } from "@polar-sh/sdk";

const polarClient = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  // 如果使用 Polar 沙盒环境,则使用 'sandbox'
  // 记住,访问令牌、产品等在不同环境间完全分隔。
  // 生产环境获取的访问令牌不能在沙盒环境使用。
  server: "sandbox",
});

const auth = betterAuth({
  // ... Better Auth 配置
  plugins: [
    polar({
      client: polarClient,
      createCustomerOnSignUp: true,
      getCustomerCreateParams: ({ user }, request) => ({
        metadata: {
          myCustomProperty: 123,
        },
      }),
      use: [
        // 这里添加 Polar 插件
      ],
    }),
  ],
});

必需选项

  • client: Polar SDK 客户端实例

可选选项

  • createCustomerOnSignUp: 用户注册时自动创建 Polar 客户
  • getCustomerCreateParams: 自定义函数,提供额外的客户创建元数据

客户管理

启用 createCustomerOnSignUp 后,当在 Better-Auth 数据库中新增用户时,系统会自动创建对应的 Polar 用户。

所有新客户均带有关联的 externalId,即数据库中用户的 ID,避免了在数据库中维护 Polar 与用户的映射。

结账插件

为了支持你的应用内结账,只需将 Checkout 插件通过 use 属性传入。

import { polar, checkout } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth 配置
    plugins: [
        polar({
            ...
            use: [
                checkout({
                    // 可选字段 - 允许通过 slug 而非产品 ID 触发结账
                    products: [ { productId: "123-456-789", slug: "pro" } ],
                    // 结账成功后跳转的相对 URL
                    successUrl: "/success?checkout_id={CHECKOUT_ID}",
                    // 是否仅允许认证用户结账
                    authenticatedUsersOnly: true
                })
            ],
        })
    ]
});

启用结账后,可以通过 BetterAuth 客户端的 checkout 方法初始化结账会话,用户将被重定向到产品结账页面。

await authClient.checkout({
  // 这里可传任何 Polar 产品 ID
  products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
  // 或者,如果你在结账配置中设置了“products”,可以传递 slug
  slug: "pro",
});

结账会自动将认证用户作为客户关联,邮箱地址将被“锁定”。

如果 authenticatedUsersOnlyfalse,则可触发无关联客户的结账会话。

组织支持

此插件支持组织插件。如果你将组织 ID 传给结账的 referenceId,你可以追踪组织成员的购买记录。

const organizationId = (await authClient.organization.list())?.data?.[0]?.id;

await authClient.checkout({
    // 这里可传任何 Polar 产品 ID
    products: ["e651f46d-ac20-4f26-b769-ad088b123df2"],
    // 或者,如果你在结账配置中设置了“products”,可以传递 slug
    slug: 'pro',
    // referenceId 会保存为结账、订单与订阅对象的 metadata 中的 `referenceId`
    referenceId: organizationId
});

门户插件

允许客户管理其购买、订单及订阅的插件。

import { polar, checkout, portal } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth 配置
    plugins: [
        polar({
            ...
            use: [
                checkout(...),
                portal()
            ],
        })
    ]
});

portal 插件为 BetterAuth 客户端提供了一组客户管理方法,位于 authClient.customer 下。

客户门户管理

以下方法可将用户重定向至 Polar 客户门户,查看订单、购买、订阅、权益等。

await authClient.customer.portal();

客户状态

portal 插件还添加了方便的状态查询方法,可获取通用客户状态。

const { data: customerState } = await authClient.customer.state();

客户状态对象包含:

  • 客户的所有数据
  • 其活跃的订阅列表
    • 注意:不含父组织的订阅。详见下方订阅列表方法
  • 授权权益列表
  • 活跃计量器列表及其当前余额

因此,凭该对象即可判定用户是否应获得服务访问权限。

你可在 Polar 文档中了解更多关于客户状态的信息

权益、订单与订阅

portal 插件提供三个方便方法,列出认证用户/客户相关的权益、订单与订阅。

所有这些方法均使用 Polar CustomerPortal API

权益

此方法仅列出认证用户/客户的已授予权益。

const { data: benefits } = await authClient.customer.benefits.list({
  query: {
    page: 1,
    limit: 10,
  },
});

订单

此方法列出认证用户/客户的订单,如购买及订阅续费。

const { data: orders } = await authClient.customer.orders.list({
  query: {
    page: 1,
    limit: 10,
    productBillingType: "one_time", // 或 'recurring'
  },
});

订阅

此方法列出认证用户/客户的订阅。

const { data: subscriptions } = await authClient.customer.subscriptions.list({
  query: {
    page: 1,
    limit: 10,
    active: true,
  },
});

重要 — 组织支持

此方法不会返回由父组织为认证用户订购的订阅。

你可以向此方法传入 referenceId,它将返回与该 referenceId 关联的所有订阅,而非用户的订阅。

因此,要判断用户是否应有权限,可传入用户所属组织的 ID,查看该组织是否有活跃订阅。

const organizationId = (await authClient.organization.list())?.data?.[0]?.id;

const { data: subscriptions } = await authClient.customer.orders.list({
    query: {
      page: 1,
      limit: 10,
      active: true,
      referenceId: organizationId
    },
});

const userShouldHaveAccess = subscriptions.some(
    sub => // 你的逻辑以检查订阅产品等。
)

用量插件

用于基于用量计费的简易插件。

import { polar, checkout, portal, usage } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth 配置
    plugins: [
        polar({
            ...
            use: [
                checkout(...),
                portal(),
                usage()
            ],
        })
    ]
});

事件摄取

Polar 的基于用量计费完全依赖事件摄取。你可以从应用中摄取事件,创建计量器表示该使用量,并为产品添加计量价格进行计费。

在 Polar 文档中了解更多关于基于用量计费

const { data: ingested } = await authClient.usage.ingest({
  event: "file-uploads",
  metadata: {
    uploadedFiles: 12,
  },
});

认证用户会自动与摄取事件关联。

客户计量器

一个简单方法列出认证用户的用量计量器,即“客户计量器”。

客户计量器包含其定义计量器的所有用量信息:

  • 客户信息
  • 计量器信息
  • 客户计量器信息
    • 已消费单位
    • 已计入单位
    • 余额
const { data: customerMeters } = await authClient.usage.meters.list({
  query: {
    page: 1,
    limit: 10,
  },
});

Webhooks 插件

Webhooks 插件可用于捕获来自你的 Polar 组织的事件。

import { polar, webhooks } from "@polar-sh/better-auth";

const auth = betterAuth({
    // ... Better Auth 配置
    plugins: [
        polar({
            ...
            use: [
                webhooks({
                    secret: process.env.POLAR_WEBHOOK_SECRET,
                    onCustomerStateChanged: (payload) => // 当客户相关信息变更时触发
                    onOrderPaid: (payload) => // 当订单付款成功时触发(购买、订阅续费等)
                    ...  // 超过 25 个细粒度 webhook 处理器
                    onPayload: (payload) => // 捕获所有事件的通用处理器
                })
            ],
        })
    ]
});

在你的 Polar 组织设置页面配置 Webhook 端点,Webhook 地址为 /polar/webhooks

将密钥添加到环境变量。

# .env
POLAR_WEBHOOK_SECRET=...

插件支持所有 Polar webhook 事件的处理:

  • onPayload - 捕获所有传入 Webhook 事件的通用处理器
  • onCheckoutCreated - 当结账创建时触发
  • onCheckoutUpdated - 当结账更新时触发
  • onOrderCreated - 当订单创建时触发
  • onOrderPaid - 当订单付款时触发
  • onOrderRefunded - 当订单退款时触发
  • onRefundCreated - 当退款创建时触发
  • onRefundUpdated - 当退款更新时触发
  • onSubscriptionCreated - 当订阅创建时触发
  • onSubscriptionUpdated - 当订阅更新时触发
  • onSubscriptionActive - 当订阅激活时触发
  • onSubscriptionCanceled - 当订阅取消时触发
  • onSubscriptionRevoked - 当订阅撤销时触发
  • onSubscriptionUncanceled - 当订阅取消被撤销时触发
  • onProductCreated - 当产品创建时触发
  • onProductUpdated - 当产品更新时触发
  • onOrganizationUpdated - 当组织更新时触发
  • onBenefitCreated - 当权益创建时触发
  • onBenefitUpdated - 当权益更新时触发
  • onBenefitGrantCreated - 当权益授权创建时触发
  • onBenefitGrantUpdated - 当权益授权更新时触发
  • onBenefitGrantRevoked - 当权益授权撤销时触发
  • onCustomerCreated - 当客户创建时触发
  • onCustomerUpdated - 当客户更新时触发
  • onCustomerDeleted - 当客户删除时触发
  • onCustomerStateChanged - 当客户状态变更时触发