Shopify Public App 订阅计费实战:用 Gadget 配置 Managed Billing

文章目录

如果你准备开发一个面向多个商家的 Shopify Public App,应用功能只是第一步。真正上线前,还需要解决:

  • 订阅套餐
  • 月付和年付价格
  • 免费试用
  • 商家选择计划
  • Shopify 计费确认
  • 订阅状态同步
  • 卸载和重新安装
  • 过期订阅拦截
  • Production 环境收费

本文使用 Gadget 配置一个 Shopify Public App,并接入 Shopify 当前的 App Pricing 计费流程,完成 Basic 和 Pro 两个套餐的订阅。

Shopify 后台中部分页面可能仍会出现 Managed Pricing 相关名称,但当前公开应用的计费方向是 Shopify App Pricing。它支持固定订阅、按使用量收费,以及固定费用和用量费用组合的计费模式。citation:Building apps

项目的整体结构

项目由 Shopify、Gadget 和 Shopify App Pricing 三部分组成:

Shopify Public App
        ↓
Shopify App Pricing
        ↓
商家选择套餐并确认收费
        ↓
Shopify 创建订阅记录
        ↓
Gadget 同步订阅状态
        ↓
应用根据状态开放功能

Gadget 主要负责:

  • Shopify OAuth
  • Embedded App
  • Session Token
  • PostgreSQL 数据库
  • Shopify Shop 模型
  • App Subscription 模型
  • 订阅状态同步
  • React 管理页面
  • 后端权限判断

Shopify 负责:

  • 应用安装
  • 应用分发
  • 商家收费确认
  • 应用版本
  • App Store 审核
  • 订阅和账单基础设施

Public App 和 Custom App 的区别

只有适合公开分发的 Public App 才适合向多个商家收费。

Public App

适合:

  • 面向多个独立商家
  • 提交 Shopify App Store
  • 提供 Basic、Pro 等套餐
  • 通过 Shopify App Pricing 计费

Custom App

适合:

  • 单个客户定制
  • 企业内部使用
  • 特定组织使用
  • 不准备公开销售

Custom App 不适合使用 Shopify Billing API 向商家收费。

应用创建时就要确认分发方向,因为 Shopify 的分发方式选择后通常不能随意切换。

开始前的准备

需要准备:

  • Shopify Partners 或开发者账号
  • Shopify Development Store
  • Gadget 账号
  • Shopify Dev Dashboard
  • Shopify App Pricing 配置权限
  • Node.js
  • VS Code 或其他代码编辑器

本项目技术栈:

Backend: Gadget Serverless Node.js
Database: Gadget PostgreSQL
Frontend: React
Admin UI: Shopify Polaris
Billing: Shopify App Pricing

开发阶段使用 Development Store 和测试收费,不要直接连接真实商店。

创建 Gadget Shopify App

进入 Gadget 应用创建页面:

https://gadget.new

选择:

Shopify App

应用名称可以设置为:

Managed Billing Demo

Gadget 会创建:

  • PostgreSQL 数据库
  • Serverless Node.js 后端
  • React 前端
  • Shopify Connection
  • Development 环境
  • Production 环境
  • Shopify 数据模型
  • Action 和 API 客户端

创建 Shopify Public App

在 Shopify Dev Dashboard 中创建应用。

应用名称可以设置为:

Managed Billing Demo

创建后获取:

Client ID
Client Secret

将它们填入 Gadget 的 Shopify Connection。

Client Secret 只能保存在服务端或 Gadget 私密配置中,不要放到:

  • React 前端
  • 浏览器 Local Storage
  • GitHub 公开仓库
  • URL 参数
  • 前端日志

配置 Shopify API Scopes

本教程的重点是计费,但为了完成 Shopify OAuth 和商店安装,仍然需要选择应用需要的权限。

如果示例应用需要读取商品,可以选择:

read_products

如果应用本身不需要商品数据,就不要额外选择商品权限。

权限应根据真实功能决定:

商品读取:
read_products

订单读取:
read_orders

库存读取:
read_inventory

商品修改:
write_products

完成 Shopify Connection 后,可以选择:

Shopify App Subscription
Shopify App

这两个模型用于帮助 Gadget 获取应用订阅和应用信息。

Shopify App Subscription 模型

Shopify App Subscription 模型用于保存商店订阅记录。

其中可能包含:

status
name
price
shop
createdAt
updatedAt

应用可以通过这些数据判断:

商店有没有订阅
订阅的是什么计划
计划是否仍然有效
订阅是否已取消

Gadget 通常会为该模型配置相关 Webhook 和同步能力。

Shopify App 模型

Shopify App 模型可以用于读取应用本身的信息,例如:

handle
name

应用 Handle 在生成计费返回地址时可能会用到。

如果当前项目已经明确知道 App Handle,也可以放进环境变量。但要确保:

  • Development 和 Production 使用不同配置
  • App Handle 与当前 Shopify App 一致
  • 不要把一个环境的 Handle 写死到另一个环境

配置 App URL 和 OAuth 回调地址

从 Gadget Shopify Connection 页面复制:

App URL
OAuth Callback URL

回到 Shopify App 配置中填写:

App URL
Allowed redirection URL

检查:

  • App URL 正确
  • OAuth 回调路径完整
  • 使用 HTTPS
  • Client ID 属于当前应用
  • Development 和 Production 没有混用
  • 当前应用版本已经保存和发布

完成配置后,先不要安装到商店,继续配置 Public App 和计费计划。

选择 Public Distribution

回到 Shopify App 的分发设置,选择:

Public distribution

Public App 后续可以提交到 Shopify App Store。

在提交和配置过程中,可能需要准备:

  • 主语言
  • 应用名称
  • 应用介绍
  • 隐私政策
  • 服务条款
  • 客服信息
  • 应用截图
  • 价格信息
  • 数据处理说明

Shopify App Pricing 的套餐配置通常会出现在应用提交或 Pricing details 页面中。

配置 App Pricing 套餐

在应用的 Pricing details 中创建套餐。

可以先创建两个计划:

Basic
Pro

示例配置:

计划月付年付
Basic$10/月$100/年
Pro$25/月$250/年

也可以设置:

  • 免费试用天数
  • 计划描述
  • 功能差异
  • 使用量收费
  • 计划可见范围
  • 订阅确认后的返回地址

价格只是示例,正式应用需要根据产品成本、服务内容和客服成本重新制定。

配置计费返回地址

商家完成 Shopify 计费确认后,Shopify 需要将商家带回应用。

可以设置一个相对路径:

/billing/callback

也可以设置 Gadget 后端的回调路由:

/api/billing/callback

本文使用后端回调路由,原因是后端可以:

  • 读取 Shopify 返回的参数
  • 查询当前商店
  • 检查订阅是否已经同步
  • 暂时保存计费引用
  • 再将商家重定向到 Embedded App

计费回调过程如下:

商家选择套餐
        ↓
Shopify 显示收费确认
        ↓
商家批准
        ↓
Shopify 回调 Gadget
        ↓
Gadget 检查商店和订阅状态
        ↓
重定向回应用首页

为什么需要临时保存 Charge ID?

计费确认和订阅 Webhook 不一定同时到达。

可能出现这样的时间差:

商家已经完成付款
        ↓
计费回调先到达
        ↓
订阅 Webhook 还没有到达

这时 Gadget 数据库中可能还没有 active App Subscription。

为了避免商家已经完成付款却被应用挡在外面,可以在 Shop 模型中添加一个临时字段:

chargeId

它的作用只是短暂记录计费流程中的引用。

注意:

Charge ID 不能作为长期订阅凭证。最终是否允许使用应用,仍然应该以有效的 App Subscription 状态为准。

创建计费回调路由

在 Gadget 后端创建路由,例如:

api/routes/billing-callback.ts

这个路由负责:

  1. 读取 Shopify 返回的商店域名
  2. 读取当前计费引用
  3. 查询 Gadget 中的 Shop 记录
  4. 查询当前是否已经存在 active 订阅
  5. 如果没有 active 订阅,临时保存计费引用
  6. 将商家重定向回 Embedded App

逻辑可以理解为:

收到计费回调
        ↓
查询当前 Shop
        ↓
检查 App Subscription
        ↓
如果已有 active 订阅
    直接进入应用
否则
    临时保存 chargeId
    再进入应用等待同步

回调路由中需要校验商店身份,不能只相信 URL 中传入的商店域名。

清理临时 Charge ID

当 Shopify App Subscription Webhook 到达后,Gadget 会创建或更新订阅记录。

如果订阅状态是:

active

就可以清空 Shop 上临时保存的:

chargeId

数据变化如下:

完成付款
        ↓
Shop.chargeId 暂时保存
        ↓
App Subscription 创建成功
        ↓
确认 status = active
        ↓
清空 Shop.chargeId

这样可以避免旧的计费引用长期留在数据库中。

处理应用卸载和重新安装

这是计费流程中非常容易忽略的部分。

商家卸载应用后,应用可能无法继续收到这个商店的部分 Webhook。因此,Gadget 本地数据库中的订阅状态可能暂时仍然显示为:

active

如果商家之后重新安装应用,而应用只相信旧的本地数据,就可能错误地让已经取消订阅的商家继续使用。

更稳妥的做法是在:

Install
Reinstall

Action 中重新同步最新的 Shopify App Subscription 数据。

流程如下:

商家重新安装应用
        ↓
运行 App Subscription 同步
        ↓
读取最新订阅状态
        ↓
更新 Gadget 数据库
        ↓
判断是否需要重新计费

重新安装时,只同步需要的模型即可:

Shopify App Subscription

不需要每次都同步全部 Shopify 数据。

创建 Plans 页面

在 Gadget React 前端中创建:

web/routes/plans.jsx

Plans 页面负责:

  • 读取可用套餐
  • 显示价格
  • 显示试用期
  • 显示计划描述
  • 读取当前订阅
  • 提供订阅按钮
  • 处理计费返回

可以使用:

  • Shopify Polaris Card
  • Layout
  • Button
  • Banner
  • Badge
  • Spinner
  • App Bridge Title Bar

页面可以显示:

Basic
$10 / month
Suitable for small stores

[Choose Basic]
Pro
$25 / month
Advanced features

[Choose Pro]

启动订阅流程

商家点击套餐按钮后,前端调用 Shopify App Pricing 的订阅流程。

过程如下:

点击 Choose Plan
        ↓
创建订阅请求
        ↓
Shopify 显示收费确认
        ↓
商家批准收费
        ↓
回调 Gadget
        ↓
同步 App Subscription
        ↓
返回应用首页

计费流程中需要处理:

  • 商家批准
  • 商家取消
  • 订阅已经存在
  • 试用期仍然有效
  • 计划升级
  • 计划降级
  • 收费失败
  • 返回地址错误
  • Webhook 延迟

不要在商家完成收费确认之前开放付费功能。

在应用首页添加订阅拦截

应用首页需要检查当前商店是否有有效订阅。

可以查询:

Shop
App Subscriptions
chargeId

判断逻辑:

如果存在 active App Subscription:
    允许进入应用

如果没有 active App Subscription:
    跳转到 Plans 页面

如果应用处于嵌入式 iframe 中,跳转时要使用正确的父窗口或 Shopify App Bridge 跳转方式,避免只在 iframe 内部加载计费页面。

推荐将这个判断做成:

  • React Provider
  • 路由守卫
  • 统一的订阅 Hook
  • 后端权限中间件

不要在每个页面中重复复制一套订阅检查代码。

前端不能代替后端检查

即使前端隐藏了按钮,也不能说明功能真正被保护。

商家可能直接调用:

API Action

因此,涉及付费权限的操作必须由后端再次检查:

当前 Shop
        ↓
当前 App Subscription
        ↓
订阅状态
        ↓
计划名称
        ↓
是否允许执行当前操作

前端负责显示体验,后端负责最终授权。

测试计费流程

在 Development Store 中完成以下测试。

安装测试

  • 应用可以安装
  • OAuth 正常
  • Shop 记录创建
  • App Subscription 模型可用
  • 计划页面可以打开

付款测试

  • Basic 计划可以选择
  • Pro 计划可以选择
  • Shopify 显示测试收费
  • 测试收费可以批准
  • 回调可以返回 Gadget
  • App Subscription 最终变为 active

延迟测试

模拟:

付款回调先到
订阅 Webhook 后到

确认:

  • chargeId 能暂时保存
  • active 订阅创建后能清理
  • 应用最终不会错误阻止已付款商家

卸载测试

  1. 安装应用
  2. 订阅 Basic
  3. 确认状态为 active
  4. 卸载应用
  5. 重新安装应用
  6. 执行订阅同步
  7. 确认取消的订阅显示为 canceled
  8. 重新选择 Pro
  9. 确认新订阅变为 active

计划状态测试

测试以下状态:

active
canceled
expired
past_due
pending

不同状态应该对应不同的应用行为。

生产环境配置

正式上线前,准备独立的:

Development Shopify App
Production Shopify App

生产环境需要重新确认:

  • Production Client ID
  • Production Client Secret
  • Production App URL
  • Production OAuth 回调地址
  • Production 数据库
  • Production 环境变量
  • Production App Pricing
  • Production 订阅回调地址

测试环境中的:

Test charges

不能用于正式商家。

生产环境还需要完成:

  • Shopify App Store 应用资料
  • 隐私政策
  • 服务条款
  • 退款政策
  • 取消订阅政策
  • 客服页面
  • 计费说明
  • 数据删除流程

常见问题排查

商家安装后直接进入应用,没有看到计费页面

检查:

  • 应用是否设置为 Public Distribution
  • Plans 是否已经创建
  • 当前商店是否有 active 订阅
  • 前端是否执行了订阅检查
  • App Handle 是否正确
  • 计费回调地址是否正确

付款后仍然显示没有订阅

检查:

  • App Subscription Webhook 是否到达
  • Shopify App Subscription 模型是否同步
  • 回调是否提前返回
  • Shop 和 App Subscription 的关系是否正确
  • 是否需要手动运行一次订阅同步
  • Gadget 日志是否有异常

已经卸载的商家仍然可以使用应用

检查:

  • 是否只依赖本地 active 记录
  • Reinstall Action 是否执行订阅同步
  • 取消订阅记录是否被更新
  • 前端是否缓存了旧订阅状态
  • 后端是否重新校验当前订阅

Charge ID 一直没有清理

检查:

  • App Subscription Create Action 是否执行
  • 是否判断 status = active
  • Shop ID 是否正确
  • 更新 Shop 记录时是否使用了正确的 ID
  • Webhook 是否存在延迟或失败

用户绕过 Plans 页面继续使用高级功能

检查:

  • 后端 Action 是否验证订阅
  • 是否只在前端做跳转
  • 当前 Shop 是否正确识别
  • 计划权限是否在服务端执行
  • 是否允许没有 active 订阅的商店调用付费功能

项目验收清单

完成以下检查后,Public App 的 Managed Billing 才算基本跑通:

  • Shopify Public App 创建成功
  • Gadget Shopify Connection 正常
  • OAuth 安装正常
  • App URL 配置正确
  • App Pricing 套餐创建成功
  • Basic 计划可以显示
  • Pro 计划可以显示
  • 计费回调路由正常
  • Shop 模型可以保存临时 chargeId
  • App Subscription 模型可以同步
  • 订阅成功后 chargeId 可以清理
  • active 订阅可以进入应用
  • 无订阅商店会跳转到 Plans 页面
  • 商家取消订阅后不能继续使用受限功能
  • 卸载后重新安装会同步最新订阅状态
  • 计划升级和降级可以处理
  • 后端会重新验证订阅权限
  • Development 和 Production 配置彼此隔离
  • Production 不使用测试收费

结语

Shopify Public App 的计费不是简单显示一个价格,而是一条完整的状态链:

商家安装应用
        ↓
查看计划
        ↓
选择套餐
        ↓
Shopify 收费确认
        ↓
计费回调
        ↓
App Subscription 同步
        ↓
应用开放功能
        ↓
取消或卸载后重新校验

Gadget 主要负责:

  • Shopify App 连接
  • Shop 数据
  • App Subscription 数据
  • 后端 Action
  • 订阅同步
  • React 管理页面
  • 生产环境

Shopify App Pricing 负责:

  • 计划展示
  • 收费确认
  • 订阅记录
  • 计费基础流程
  • 应用分发和审核相关能力

真正需要特别注意的是三个细节:

不要把 chargeId 当成永久授权
不要只依赖前端判断订阅
卸载后重新安装必须同步最新订阅状态

只要把安装、付款、回调、Webhook、卸载和重新安装这些环节都测试一遍,应用的计费流程才算真正具备生产可用性。

citation:Building apps