跳转到内容
彼岸论坛
欢迎抵达彼岸 彼岸花开 此处谁在 -彼岸论坛

[程序员] SaaS 产品集成 Stripe 支付的一些坑


小天管理

已推荐帖子

SaaS 产品集成 Stripe 支付的一些坑

最近在给自己的 SaaS 产品 PPResume 集成支付,调研了很久,最终还是决定用 Stripe ,集成过程中又发现了一些坑,分享出来,供后来者参考。

四种集成方式的选择

Stripe 官方提供四种主流的集成方式

  • payment links:最简单的一种,集成成本最低,时间最快,但是可定制化程度最低,比较适合简单的业务
  • checkout ,又分为 hosted checkout 和 embedded checkout 两种:
    • hosted checkout:用户在 checkout 时,需要重定向到 stripe 的网站,好比说你在淘宝买东西付款时,需要重定向到支付宝或者微信支付的网站,用户流程更长一些,集成成本也不算高,比较简单
    • embedded checkout:stripe 提供一个内嵌的 form ,可以直接嵌入到你的网站中,这样用户在 checkout 时,不需要重定向到 stripe ,用户体验会更好一些,集成成本比 hosted checkout 高很多,我个人开发过程中的一个 demo
  • elements:定制化最强的一种,同时也是成本最高的,所有的元素,诸如信用卡输入框这些均可定制,适合高度复杂的业务,不建议普通用户使用

Next.js 官方有一个 demo,可以直观对比下 payment links 、checkout 和 elements 三种方式的区别

我个人建议:

  • 如果是极其简单的业务,或者开发时间、能力有限,用 payment links 就行,比如说你在网上卖电子书、软件 license ,放个 payment links ,收到 payment 后,给 customer 自动或者手动发送电子书、软件 license 就行了; payment links 还有一个显著的优点,就是因为 payment links 本身提供的是一个 URL ,你可以把这个 URL 分享到各种平台,就如我们日常在用的二维码收款码一样
  • 如果略复杂一些的业务,比如有多种 pricing plan ,可以用 checkout ,想要最佳的用户体验,又能承担高一些的开发成本,可以用 embedded checkout ,但是 checkout 有个关联 customer 的小坑,后面说
  • elements 不建议采用,开发成本过高了

Pricing Table 的问题

Stripe 提供一个 no code 的 pricing table,可以方便的在自己的网站上嵌入一个 pricing table 。但是 Stripe 的 pricing table 有一个问题,就是默认点击 Subscribe button 的时候,是直接定向到 stripe 的 checkout 页,而对一般的 SaaS 产品而言,Pricing Table 的 CTA button 针对用户是否已经登录,要定向到不同的页面:

  • 如果用户尚未注册登录,则直接重定向到用户的注册登录页,引导用户注册登录
  • 如果用户已经登录,则按照正常的流程,重定向用 subscribe 付款页面

Stripe 的 Pricing Table 另一个问题就是 UI 的可定制性还是差了一些,没有提供单独的 CSS 定制,可能和产品本身的 UI 风格有较大差异。

海外有一个专门的 Pricing Table SaaS 产品,部分解决了这个问题,可以参考一下。实在不行,Pricing Table 就还是自己写的,不难的。

Checkout 最好关联 Customer

Stripe Checkout Session创建 时,接收一个 customer 参数,该参数是 stripe 内建的 customer 的 id ,如果创建 Stripe Checkout Session 时(stripe.checkout.sessions.create),没有提供这个 customer 参数,那么 Stripe 的后台会将所有 email/credit card/phone 相同的 customer 统一归组成一个 guest customer ,这个 guest customer 仅仅在 Stripe Dashboard 中可见,而在 Stripe API 中是不可见的,并且 guest customer 没有办法对接 Customer Portal (关于 Customer Portal ,见后文)。

我个人建议:

  • 在创建 stripe checkout session 时,先创建 stripe customer ,但是不要用一个 customer email 创建多个 Stripe customer ,最后确保一个 customer email 创建并且仅创建一个 Stripe Customer ,code 类似于这种:
/**
 * Retrieves or creates a Stripe customer with logto user's email.
 *
 * This function first attempts to fetch the user's context from the Logto
 * client using the provided request.
 *
 * Then checks if a customer already exists in Stripe with that email, if
 * existed, return that stripe customer, otherwise a new customer is created in
 * Stripe.
 *
 * @param {NextRequest} request - The incoming request object from Next.js.
 *
 * @returns {Promise<Stripe.Customer | null>} A promise that resolves to the
 * Stripe Customer object if found or created, otherwise null.
 */
export async function getOrCreateCustomer(
  request: NextRequest
): Promise<StripeServer.Customer | null> {
  const stripe = getStripeServer()

  try {
    // find a way to get a customer email from your auth system
    const customerEmail = 'customer@ppresume.com'

    // This is not likely to happen as all logto users should have a valid email
    if (!customerEmail) {
      return null
    }

    const customers = await stripe.customers.list({
      email: customerEmail,
      limit: 1,
    })

    if (customers.data.length === 0) {
      return await stripe.customers.create({
        email: customerEmail,
        metadata: {
          logtoUserId: user?.claims!.sub,
        },
      })
    } else {
      return customers.data[0]
    }
  } catch (err) {
    return null
  }
}

Customer Portal 集成的两种方式

Stripe 提供一个 no code 的 Customer Portal,可以让用户自行管理支付相关的数据,比如支付方式,订阅记录,invoices 等等,这些东西自己实现,又繁琐又无聊,因此如果支付系统能提供相应的解决方案,还是很方便的。

据我所知,Stripe 和 LemonSqueezy 是提供 customer portal 的,而 Paddle 是不提供的(有一个专门的 SaaS 产品就是给 Paddle 提供一个 Customer Portal )。

集成 Stripe Customer Portal 有两种方式:

  • 一种是通过 no-code 的方式,在 Stripe Dashboard 中直接 activate customer link ,拿到一个 URL ,将这个 URL 放到你的网站中,用户访问这个 URL ,就可以用验证码登录来访问或管理支付相关的信息
  • 另一种是通过 API 的方式,在用户访问 customer portal 时,通过 API 临时创建一个 customer portal 的 URL ,然后把这个临时 URL 返回给用户,用户通过这个临时生成的 URL 访问 customer portal ,代码大略如下:
/**
 * Creates a Stripe customer portal session and redirects user to that portal
 *
 * It first retrieves or creates a Stripe customer based on the incoming
 * request, then generates a session for the Stripe Billing Portal, and finally
 * redirects the user to the Stripe Billing Portal.
 *
 * The major benefit of using API to create a customer portal with customer
 * attached over plain customer portal link provided from stripe dashboard is,
 * user won't need to manually sign in for API created customer portal.
 * Basically, when you create a customer portal with API, it will generate a
 * short-lived URL for user and this URL do not need user to sign in.
 *
 * From [Stripe]( https://docs.stripe.com/api/customer_portal/sessions):
 *
 * ```
 * A portal session describes the instantiation of the customer portal for a
 * particular customer. By visiting the session’s URL, the customer can manage
 * their subscriptions and billing details. For security reasons, sessions are
 * short-lived and will expire if the customer does not visit the URL. Create
 * sessions on-demand when customers intend to manage their subscriptions and
 * billing details
 * ```
 *
 * @param {NextRequest} request - The incoming request object from Next.js.
 *
 * @returns {Promise<Response>} A promise that resolves to a redirect response
 * to the Stripe Billing Portal, or a JSON response indicating that the customer
 * was not found.
 */
export async function GET(request: NextRequest) {
  const stripe = getStripeServer()
  const customer = await getOrCreateCustomer(request)

  if (!customer) {
    return Response.json(
      {
        message: 'Customer not found',
      },
      { status: 404 }
    )
  }

  const customerPortalSession = await stripe.billingPortal.sessions.create({
    customer: customer.id,
    return_url: getAbsoluteUrlWithDefaultBase(routes.pages.settings.billing),
  })

  return Response.redirect(customerPortalSession.url, 303)
}

第二种方式最大的好处就是,用户不必通过验证码来登录 customer portal ,而是可以直接访问,用户体验要好太多

Stripe Webhook 实现

用户下单之后,系统需要满足用户的下单需求,比如如果是实物电商,那么商家需要打包发货,而如果是 SaaS 这种虚拟服务,则一般需要赋予用户相应的 Pro 权限,这个过程叫做 order fulfillment

Order fulfillment 需要实现 webhook:

Webhooks are required

You can’t rely on triggering fulfilment only from your Checkout landing page, because your customers aren’t guaranteed to visit that page. For example, someone can pay successfully in Checkout and then lose their connection to the internet before your landing page loads.

Set up a webhook event handler to get Stripe to send payment events directly to your server, bypassing the client entirely. Webhooks are the most reliable way to know when you get paid. If webhook event delivery fails, we retry several times.

所谓 webhook ,简单讲,就是在你的系统中向公网暴露出一个 API endpoint ,当用户通过 Stripe 下单成功后,Stripe 给这个 API endpoint 发送一个 POST 请求,请求 event 里包括详细的下单付款信息,你的系统收到这个信息后决定怎样给用户赋予相应的权限等等。

Webhook 实现中有一个小坑,就是 Stripe 不保证 event 的时序性,也不保证请求的唯一性——事实上,公网的网络请求中也是没有办法做到这一点的,因此你的 Webhook 实现中,看需求,如果对数据一致性要求比较高,最好实现幂等性。具体细节不在这里讲啦。


后话:集成 Stripe 花了小一个月的时间,Stripe 的开发体验非常不错,但是产品矩阵很庞大,新手往往比较容易困惑,再就是有一些隐藏的坑,我遇到了一些,随手记了下,发上来当一个备份吧。

集成支付系统还是蛮耗体力的一个活,后面有时间的话,打算写一篇《 Stripe 101:The Missing Tutorial for Indie Makers 》。

最后打个小广告哈,我的 SaaS 产品 PPResume 去年上线后又经过一年的打磨,最近终于要推出付费计划啦,所有在支付正式上线之前注册的用户,都将获得一年内 50% off 的折扣,欢迎注册体验。

意见的链接
分享到其他网站

加入讨论

您现在可以发表并稍后注册. 如果您是会员,请现在登录来参与讨论.

游客
回复主题...

×   粘贴为富文本.   粘贴为纯文本来代替

  只允许使用75个表情符号.

×   您的链接已自动嵌入.   显示为链接来代替

×   您之前的内容已恢复.   清除编辑器

×   您无法直接粘贴图片.要从网址上传或插入图片.

  • 游客注册

    游客注册

  • 会员

    没有会员可显示

  • 最新的状态更新

    没有最新的状态更新
  • 最近查看

    • 没有会员查看此页面.
×
×
  • 创建新的...