深色模式
iOS自动续期订阅
概述
iOS 自动续期订阅的新接入方式以 StoreKit 2 为 App 端核心,以 App Store Server API 和 App Store Server Notifications V2 为服务端核心。App 负责展示商品、发起购买、监听交易变化;Server 负责验证 Apple 签名、维护用户权益、接收订阅生命周期事件。
本文按 2026-04-26 查阅的 Apple Developer 文档整理。旧的 verifyReceipt、共享密钥校验和 App Store Server Notifications V1 只适合作为历史项目迁移背景,新项目应优先使用 JWS 签名交易、App Store Server API 和 V2 通知。
接入边界
自动续期订阅一般分成三块:
- App Store Connect:创建订阅组、订阅商品、订阅等级、价格、可售地区、优惠和 Server Notifications URL。
- App:使用 StoreKit 2 获取商品、发起购买、监听交易、展示当前权益和恢复购买入口。
- Server:验证 JWS,维护用户与交易的绑定关系,接收 V2 通知,并用 App Store Server API 查询或修复状态。
如果订阅权益只影响本机 UI,App 可以用 StoreKit 2 的本地验证结果判断访问权。但只要权益会影响服务端资源,例如会员接口、云端额度、AI 次数、内容库访问,Server 就应该作为最终判断方。
Server Notifications URL 在 App Store Connect 的 Apps -> 目标 App -> App Information -> App Store Server Notifications 中配置。生产环境和沙盒环境可以分别设置 URL,新接入选择 Version 2。
订阅规则
自动续期订阅必须放在订阅组里。同一个订阅组里,用户同一时间只能订阅一个商品。大多数 App 应该只创建一个订阅组,这样用户不会误买多个互斥会员;只有用户确实可以同时购买多个独立权益时,才适合拆成多个订阅组。
订阅组内用等级决定套餐切换规则。Apple 建议把权益最高的套餐放在 level 1,权益较低的套餐放在后续等级。
| 行为 | 规则 |
|---|---|
| 升级 | 切到更高等级,通常立即生效,原订阅按比例退款,新订阅从升级日开始计算续期日。 |
| 降级 | 切到更低等级,当前订阅继续可用,到下一个续期日后才变成低等级。 |
| 平级切换 | 同等级、同周期的切换通常立即生效;同等级、不同周期的切换通常在下一个续期日生效。 |
订阅周期只能选 Apple 支持的周期:1 week、1 month、2 months、3 months、6 months、1 year。订阅周期提交审核后不能再改,后续只能创建新商品替代。
取消订阅不等于立即失效。用户在系统订阅管理页关闭自动续期后,当前已付费周期仍然有效,App 和 Server 不应该立刻移除权益。真正影响权益的是到期、退款、撤销、账单失败且没有宽限期等状态。
账单失败后,订阅可能进入 Billing Retry。若开启 Billing Grace Period,用户在宽限期内仍应继续获得服务;宽限期结束仍未恢复付款时,再移除权益。退款或撤销会带来 revocationDate 等信息,这类事件需要及时收回权益。
订阅优惠包括 introductory offer、promotional offer、offer code 和 win-back offer。优惠是否可用、是否已兑换,不要只靠客户端 UI 文案判断,应结合 StoreKit 返回的交易信息、Server API 的订阅状态和服务端业务规则处理。
事件模型
自动续期订阅不是一次购买回调就结束的流程。购买、续期、取消自动续期、账单失败、宽限期、恢复付款、退款、套餐切换,都可能在 App 外发生。
| 来源 | 主要用途 |
|---|---|
Product.PurchaseResult | 处理用户在当前设备发起的购买结果。 |
Transaction.updates | 监听 App 外、其他设备、Ask to Buy、优惠码兑换等交易变化。 |
Transaction.currentEntitlements | 在启动、登录、恢复购买后刷新当前有效权益。 |
| App Store Server Notifications V2 | Server 近实时接收购买、续期、退款、状态变化等事件。 |
| App Store Server API | 查询当前订阅状态、交易历史、通知历史,用于校验和补偿。 |
事件处理应按“最终状态”建模,而不是按“收到哪个回调就加减权益”建模。通知可能重试,沙盒通知可能只发一次,用户也可能在 App 外完成管理操作,所以 Server 应能随时通过 Get All Subscription Statuses 拉取当前状态。
App 事件
App 端推荐使用 StoreKit 2。最低限度需要处理四类动作:获取商品、发起购买、监听交易、刷新权益。
购买时,purchase 的结果不能简单当成成功或失败:
.success(.verified(transaction)):交易通过 StoreKit 验证,可以根据业务发放权益,并把transaction.jwsRepresentation发送给 Server 验证和入库。.success(.unverified(_, _)):交易未通过验证,不发放权益。.pending:交易未完成,常见于 Ask to Buy,不发放权益。Interrupted purchase 这类中断购买可能先失败,后续再通过Transaction.updates投递最终交易。.userCancelled:用户取消购买,不发放权益。
交易监听要尽早启动,通常在 App 启动后创建后台 Task 监听 Transaction.updates。Apple 文档明确提醒,如果不尽早监听,可能错过启动时投递的未完成交易。
swift
final class PurchaseObserver {
private var task: Task<Void, Never>?
func start() {
task = Task(priority: .background) {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else {
continue
}
await applyEntitlement(transaction)
await transaction.finish()
}
}
}
}finish() 的含义是告诉 App Store:App 已经交付内容或启用服务。订阅项目通常应在本地完成权益更新,或 Server 已可靠接收交易后再调用。不要在未处理交易前直接 finish()。
Transaction.currentEntitlements 适合用来恢复当前权益。它会返回当前仍有权益的非消耗型项目和自动续期订阅;已退款或撤销的项目不会出现在当前权益里。恢复购买按钮可以调用 AppStore.sync(),但不建议在每次启动时静默调用,因为它可能触发系统登录或交互。
如果 App 有自己的账号体系,购买时建议传入 appAccountToken。这个值应由 Server 生成并与用户绑定,格式是 UUID。后续 Server 可以用交易里的 appAccountToken 辅助识别用户,尤其是处理跨设备、App 外购买和通知事件时。
Server 事件
App Store Server Notifications V2 会向服务端发送 POST 请求,请求体只有一个核心字段:signedPayload。它是 Apple 签名的 JWS。解开后可以得到 notificationType、subtype、notificationUUID、data.environment、data.signedTransactionInfo 和 data.signedRenewalInfo 等信息。
常见订阅事件可以按下面方式理解:
| 事件 | 含义 | 处理 |
|---|---|---|
SUBSCRIBED | 首次订阅或重新订阅。 | 验证交易,创建或恢复权益。 |
DID_RENEW | 自动续期成功。 | 更新到期时间;BILLING_RECOVERY 表示账单恢复后续期成功。 |
DID_FAIL_TO_RENEW | 续期失败。 | 检查是否有 GRACE_PERIOD,有宽限期则继续服务。 |
GRACE_PERIOD_EXPIRED | 宽限期结束。 | 再查当前状态,确认未恢复后移除权益。 |
EXPIRED | 订阅过期。 | 按 VOLUNTARY、BILLING_RETRY、PRICE_INCREASE 等原因记录流失。 |
DID_CHANGE_RENEWAL_STATUS | 用户或系统打开、关闭自动续期。 | 只更新续期状态,不要因为关闭自动续期立即停权。 |
DID_CHANGE_RENEWAL_PREF | 用户更改下次续期的套餐。 | 记录套餐变更意图,权益以当前交易和状态 API 为准。 |
OFFER_REDEEMED | 用户兑换订阅优惠。 | 记录优惠信息,并结合交易状态决定权益。 |
PRICE_INCREASE | 价格上涨通知或用户同意状态变化。 | 记录价格同意状态,必要时提示用户处理。 |
REFUND | Apple 完成退款。 | 按交易撤销或调整权益。 |
REFUND_REVERSED | 退款撤回。 | 重新校验当前状态后恢复权益。 |
REVOKE | Family Sharing 等权益被撤销。 | 移除对应共享权益。 |
CONSUMPTION_REQUEST | Apple 请求消费数据以辅助退款决策。 | 按业务需要调用 Send Consumption Information。 |
TEST | 测试通知。 | 只用于验证 webhook 是否可达。 |
这张表不是 V2 通知类型全集。Apple 会持续扩展通知类型和字段,服务端代码应对未知 notificationType 做兼容记录,不要因为未知事件直接返回失败。
Webhook 处理建议:
- 使用 Apple 的 App Store Server Library 验证并解码
signedPayload。 - 继续验证
signedTransactionInfo和signedRenewalInfo。 - 校验
bundleId、appAppleId、environment是否属于当前 App 和当前环境。 - 使用
notificationUUID做通知幂等,重复通知直接返回成功。 - 使用
transactionId、originalTransactionId、webOrderLineItemId维护订阅链路。 - 成功持久化后返回
200到206;未处理成功时返回4xx或5xx让生产环境重试。
V2 通知在生产环境会重试五次,间隔大致是前一次失败后的 1、12、24、48、72 小时。沙盒环境没有这套重试机制,通常只投递一次,所以沙盒测试时更要依赖日志和 Get Notification History 排查。
权益判断
Server 判断订阅权益时,优先使用 App Store Server API 的 Get All Subscription Statuses。它返回自动续期订阅的当前状态,比单独处理某一条通知更适合做最终判断。
常见状态可以这样落库:
| 状态 | 含义 | 权益 |
|---|---|---|
1 | Active | 有权益。 |
2 | Expired | 无权益,除非同组有其他 active 订阅。 |
3 | Billing Retry | 付款重试中,通常无权益;若业务自定义延长,需要明确记录。 |
4 | Billing Grace Period | 宽限期内,有权益,到 gracePeriodExpiresDate 后再复查。 |
5 | Revoked | 已撤销,无权益。 |
单条交易也要检查关键字段:
expiresDate:订阅到期时间。revocationDate:退款或撤销时间,存在时通常应移除权益。isUpgraded:该交易已被更高等级订阅替代时,不应继续按它发放权益。productId:订阅商品 ID。originalTransactionId:同一订阅链路的原始交易 ID。appAccountToken:App 自有账号绑定标识。
App 端可用 Transaction.currentEntitlements 做即时 UI 刷新;Server 端应以 JWS 验证结果和 Server API 查询结果为准。客户端传来的 productId、用户 ID、会员等级都只能当作请求参数,不能当作可信事实。
验证方式
新版验证的核心是 JWS。StoreKit 2、App Store Server API、App Store Server Notifications V2 返回的交易和订阅续期信息,都使用 Apple 签名的 JWS 表达。
App 端验证由 StoreKit 封装成 VerificationResult。只有 .verified 才表示 StoreKit 在设备上验证通过;.unverified 不应该发放权益。即便 App 已验证通过,涉及 Server 权益时仍建议把 jwsRepresentation 发送给 Server 再验一次。
Server 端优先使用 Apple 官方 App Store Server Library。它支持 Swift、Java、Python 和 Node.js,能处理 JWT 请求签名、JWS 验证、交易解码、续期信息解码等工作。
Server 验证流程可以按这个顺序实现:
- App 购买成功后上传
transaction.jwsRepresentation。 - Server 验证 JWS 签名并解码交易。
- 校验
bundleId、appAppleId、environment、productId、appAccountToken。 - 用
transactionId调用Get All Subscription Statuses,拿到当前订阅状态。 - 更新本地订阅表和权益表。
- 后续由 V2 通知和定时补偿任务持续修正状态。
不要把“收到通知”当作验证完成。通知本身也要验签,通知里的交易和续期信息也要验签。更不要只靠前端传来的“购买成功”布尔值开会员,那个按钮挺诚实,但攻击者不一定诚实。
沙盒测试
沙盒环境使用 App Store 的真实基础设施和 App Store Connect 中的商品信息,但不会产生真实扣款。开发签名包和 TestFlight 包都会走沙盒交易;StoreKit Testing in Xcode 则是本地测试环境,不依赖 App Store 服务器,适合更早期的逻辑调试。
| 环境 | 适合场景 |
|---|---|
| StoreKit Testing in Xcode | 本地快速验证购买、续期、退款、Ask to Buy、价格上涨、账单失败等逻辑,不依赖 App Store Connect 商品同步。 |
| Sandbox | 使用 App Store Connect 商品、Sandbox Apple Account、沙盒 Server API 和沙盒通知,验证真实链路。 |
| TestFlight | 验证接近分发包的体验;默认每天续期一次,也可以登录 Sandbox Apple Account 使用沙盒控制项。 |
准备条件
沙盒测试前先确认这些条件:
- Apple Developer Program 账号有效。
- Account Holder 已签署 Paid Applications Agreement,否则内购商品可能不可用。
- App Store Connect 已创建订阅组和订阅商品,至少配置商品引用名、
productId、本地化名称和价格。 - 真机已开启 Developer Mode。开发签名包需要这个开关,TestFlight 调试沙盒控制项也会用到
Settings里的Developer菜单。 - App 端已接入 StoreKit 2,并在启动时监听
Transaction.updates。 - Server 已配置沙盒环境的 Apple JWS 验证、App Store Server API JWT、Webhook 日志和幂等处理。
App Store Connect 的商品元数据变更进入沙盒最长可能需要约一小时。刚创建商品后,Product.products(for:) 查不到商品,不一定是代码错,可以先等同步完成,再检查商品 ID、Bundle ID、协议和可售地区。
测试账号
Sandbox Apple Account 在 App Store Connect 的 Users and Access -> Sandbox 中创建。邮箱不能是已经作为 Apple Account 使用过、或买过 iTunes / App Store 内容的邮箱。账号创建后,姓名、邮箱和密码不能再编辑;需要换信息时,直接创建新测试账号更干净。
创建时要选择 App Store country or region。这个地区会影响店面、价格、本地化和商品可售性。需要测不同地区时,可以在同一个 Sandbox Apple Account 上切换 country or region,切换后在设备上退出并重新登录沙盒账号,让设备刷新店面信息。
开发签名包和 TestFlight 的登录方式不同:
| 包类型 | 登录方式 |
|---|---|
| 开发签名包 | 第一次发起购买时,系统会要求登录 Sandbox Apple Account;之后也可以在 Settings -> Developer 中管理沙盒账号。无需退出设备上的正式 Apple Account。 |
| TestFlight 包 | TestFlight 包默认使用沙盒环境。若要使用沙盒控制项,需要先在 Media & Purchases 退出正式 Apple Account,再到 Settings -> Developer 登录 Sandbox Apple Account。 |
购买弹窗里出现 [Environment: Sandbox] 或 App Store [Sandbox] 才表示当前走的是沙盒环境。没有这个标记时,不要继续点支付,应先排查账号和包类型,别把测试做成生产事故。使用 TestFlight 沙盒控制项前要先退出 Media & Purchases 的正式账号,这可能影响设备上生产 App 的已购内容访问,最好用专门的测试机。
账号设置
沙盒账号的测试行为可以在两处配置:
- App Store Connect:
Users and Access->Sandbox-> 选择测试账号。 - iOS 设备:
Settings->Developer->Sandbox Apple Account->Manage。
常用设置如下:
| 设置 | 作用 |
|---|---|
| Subscription Renewal Rate | 调整自动续期速度,同时影响 Billing Retry 和 Billing Grace Period 的时长。 |
| Interrupt Purchases | 模拟购买被打断,例如需要同意新条款或更新付款方式。 |
| Allow Purchases & Renewals | 在 iOS 沙盒账号设置里关闭后,购买会失败,订阅续期会进入账单失败流程。 |
| Clear Purchase History | 清空该测试账号的沙盒购买历史,用于重复测试首购、试用资格和重新订阅。 |
| Test Transactions | 从 App 外模拟购买、续期、重新订阅等交易,用来验证 Transaction.updates。 |
| Sandbox Test Family | 测试 Family Sharing 共享订阅权益。 |
清空购买历史后,设备上也要退出 Sandbox Apple Account 再重新登录,以清掉本地缓存。购买历史很多时,清理可能需要几分钟以上。清空只影响沙盒账号,不影响真实用户的 App Store 购买。
续期速度
Sandbox Apple Account 可以设置订阅续期速度。默认档位是 1 month = 5 minutes。同一个沙盒订阅最多自动续期 12 次,第 13 次续期尝试时自动续期会关闭。
| 续期档位 | 1 week | 1 month | 2 months | 3 months | 6 months | 1 year | Billing Retry | Billing Grace Period |
|---|---|---|---|---|---|---|---|---|
每 3 minutes | 3 minutes | 3 minutes | 6 minutes | 9 minutes | 18 minutes | 36 minutes | 全部 6 minutes | 全部 3 minutes |
每 5 minutes 默认 | 3 minutes | 5 minutes | 10 minutes | 15 minutes | 30 minutes | 1 hour | 全部 10 minutes | 1 week 为 3 minutes,其他为 5 minutes |
每 30 minutes | 10 minutes | 30 minutes | 1 hour | 1 hour 30 minutes | 3 hours | 6 hours | 全部 1 hour | 1 week 为 10 minutes,其他为 30 minutes |
每 1 hour | 15 minutes | 1 hour | 2 hours | 3 hours | 6 hours | 12 hours | 1 week 为 15 minutes,其他为 1 hour | 1 week 为 1 hour,其他为 2 hours |
这个表影响测试等待时间。比如月订阅在默认档位下,购买后大约每 5 minutes 产生一次续期交易;如果要快速测过期,可以买 1 week 周期商品,默认约 3 minutes 续期一次。
基础流程
基础流程用于确认 App、Server、通知三条链路能对上。
- 在 App Store Connect 创建一个月订阅和一个年订阅,放在同一个订阅组里。
- 在 Sandbox Apple Account 上选择默认续期档位。
- 用开发签名包或 TestFlight 包启动 App,确认能拉到订阅商品。
- 购买月订阅。
- App 端应收到
.success(.verified(transaction)),本地展示会员权益,并把transaction.jwsRepresentation发给 Server。 - Server 验证 JWS 后,检查
environment是Sandbox,并调用沙盒Get All Subscription Statuses。 - Webhook 应收到
SUBSCRIBED,subtype通常是INITIAL_BUY。 - 等待一个沙盒续期周期,Webhook 应收到
DID_RENEW,Server 里的expiresDate应向后推进。 - App 冷启动后,通过
Transaction.currentEntitlements仍能恢复当前权益。
每次测试都记录四个 ID:transactionId、originalTransactionId、webOrderLineItemId、appAccountToken。排查订阅问题时,这几个字段比“我刚刚点了一下购买”可靠得多。
续期关闭
关闭自动续期用于测试“用户取消订阅,但当前周期仍有效”。
- 先购买一个订阅,并确认当前状态为 active。
- 打开系统订阅管理页,或在 App 中调用
showManageSubscriptions(in:)让用户进入订阅管理。 - 取消订阅,也就是关闭自动续期。
- Server Notifications V2 通常会收到
DID_CHANGE_RENEWAL_STATUS,subtype为AUTO_RENEW_DISABLED。 - App 和 Server 不应立即移除权益,应继续服务到当前
expiresDate。 - 等沙盒周期到期后,再处理
EXPIRED,并用Get All Subscription Statuses复核状态。
如果用户在到期前重新打开自动续期,通知通常是 DID_CHANGE_RENEWAL_STATUS 加 AUTO_RENEW_ENABLED。这只表示续期意图变化,权益仍应以当前交易和订阅状态为准。
套餐切换
同一订阅组里的升级、降级和平级切换都要测。
| 场景 | 测试动作 | 期望 |
|---|---|---|
| 升级 | 从低等级月订阅切到高等级年订阅。 | 高等级通常立即生效;旧交易可能出现 isUpgraded,Server 应按新交易发放权益。 |
| 降级 | 从高等级订阅切到低等级订阅。 | 当前周期保持高等级,到下个续期点再变为低等级。 |
| 平级同周期 | 在同等级、同周期商品间切换。 | 通常立即生效。 |
| 平级不同周期 | 在同等级、不同周期商品间切换。 | 通常到下个续期点生效。 |
测试切换时,不要只看 App 页面上的套餐名。Server 要重新查询订阅状态,检查当前生效的 productId、下一次续期的 autoRenewProductId、旧交易的 isUpgraded,并确认权益等级和到期时间都符合预期。
账单失败
Billing Retry 和 Billing Grace Period 是订阅最容易写错的分支。沙盒里可以通过 Allow Purchases & Renewals 模拟付款失败。
测试 Billing Retry:
- 确认 App Store Connect 没有为沙盒开启 Billing Grace Period,或先关闭该配置。
- 成功购买一个自动续期订阅。
- 在 iOS 设备打开
Settings->Developer->Sandbox Apple Account->Manage->Account Settings。 - 关闭
Allow Purchases & Renewals。 - 等待下一个沙盒续期点。
- 续期失败后,订阅进入 Billing Retry。没有宽限期时,用户不应继续获得订阅服务。
- Server 用
Get All Subscription Statuses复核状态,状态应进入3,即 Billing Retry。
测试 Billing Grace Period:
- 在 App Store Connect 的订阅配置里启用 Billing Grace Period,并选择
Only Sandbox Environment。 - 成功购买订阅。
- 在设备上关闭
Allow Purchases & Renewals。 - 等待下一个续期点。
- 订阅进入宽限期,Server API 状态应进入
4,即 Billing Grace Period。 - 宽限期内继续发放权益,并记录
gracePeriodExpiresDate。 - 宽限期结束后仍未恢复付款,订阅会留在 Billing Retry,权益应停止。
测试恢复付款:
- 先让订阅进入 Billing Retry 或 Billing Grace Period。
- 回到沙盒账号设置,重新打开
Allow Purchases & Renewals。 - 等待下一次续期尝试成功。
- Server Notifications V2 通常会收到
DID_RENEW,subtype可能是BILLING_RECOVERY。 - Server 重新查询状态,恢复 active 权益。
从 Billing Retry 恢复时,新的订阅周期通常从恢复日开始;如果在 Billing Grace Period 内恢复,原订阅周期通常不变。权益代码不要假设“恢复后一定从原到期日顺延”。
App 外交易
很多订阅事件不是从 App 内购买按钮触发的。沙盒可以用 Test Transactions 模拟 App 外交易:
- 打开
Settings->Developer->Sandbox Apple Account->Manage。 - 确认
Allow Purchases & Renewals是开启状态。 - 进入
Test Transactions。 - 输入商品的
productId和 App 的 Bundle ID。 - 确认 App Store
[Sandbox]支付弹窗。 - 再打开 App,检查
Transaction.updates是否收到新交易。 - Server 检查是否收到对应通知,或通过 Server API 拉到交易。
这个用例用来验证 App 启动后能处理“其他设备购买、App Store 订阅页重新订阅、优惠兑换、自动续期”等事件。只在购买按钮回调里发会员,迟早会漏单。
中断购买
Interrupted purchase 用来模拟用户需要离开 App 完成某个动作后,购买才能继续,例如同意新条款或处理付款方式。
- 在 App Store Connect 的 Sandbox 测试账号详情里勾选
Interrupt Purchases for This Tester,或在 iOS 沙盒账号设置里启用。 - 用这个账号在 App 内购买订阅。
- 系统会先展示支付弹窗,然后中断购买,要求用户完成 App 外动作。
- App 不应提前发放权益,也不能把该状态当作永久失败;这次
purchase可能先返回失败。 - 用户完成动作后,StoreKit 会为同一商品重新投递交易;App 应通过
Transaction.updates接收并处理。 - 测完后关闭该设置,否则后续购买都会被打断。
Ask to Buy 也需要覆盖,但批准和拒绝更适合用 StoreKit Testing in Xcode 测。Xcode 的交易管理器可以直接 approve 或 decline,沙盒测试不适合作为这条分支的主验证方式。
退款测试
沙盒支持测试退款请求。App 可以在订单详情或帮助页面调用 beginRefundRequest(in:)、beginRefundRequest(for:in:),SwiftUI 项目也可以使用 refundRequestSheet。
| 退款场景 | 沙盒操作 | 期望 |
|---|---|---|
| 全额退款 | 在退款表单选择任意常规原因并提交。 | 沙盒自动批准;App 收到带 revocationDate 的交易,Server 收到 REFUND。 |
| 拒绝退款 | 选择 Other,文本输入 DECLINE,再提交。 | 沙盒自动拒绝;Server 收到 REFUND_DECLINED。 |
| 按比例退款 | 选择 Other,文本输入 GRANT_PRORATED,再提交。 | 沙盒按剩余时间计算比例;Server 收到 REFUND,交易里带按比例退款信息。 |
| 服务端消费信息 | 收到 CONSUMPTION_REQUEST 后调用 Send Consumption Information。 | 沙盒要求在约 5 minutes 内响应,结果由 ConsumptionRequest 中的字段影响。 |
退款测试要同时看 App 和 Server。App 端检查 Transaction.currentEntitlements 是否移除权益;Server 端检查 revocationDate、revocationReason、revocationType 等字段,并更新权益状态。
通知验证
沙盒通知要单独测,不要只靠购买流程顺带验证。
- 在 App Store Connect 的
App Information->App Store Server Notifications配置Sandbox Server URL,选择Version 2。 - Webhook 必须是公网可访问的 HTTPS 地址,TLS 至少为
1.2。 - Server 接收
signedPayload后,按 V2 JWS 验签并解码。 - 调用 App Store Server API 的
Request a Test Notification,确认收到TEST通知。 - 用
Get Test Notification Status查询测试通知结果。 - 购买、续期、取消、退款时,检查通知里的
data.environment是否为Sandbox。 - 通知丢失或服务宕机后,用
Get Notification History和Get All Subscription Statuses补偿。
生产环境的 V2 通知失败后会重试五次,沙盒环境通常只投递一次。沙盒测试时 webhook 短暂不可用,就可能直接错过通知,所以日志、通知历史和 Server API 补偿必须一起准备。
Server API 的沙盒基础地址是:
text
https://api.storekit-sandbox.itunes.apple.com/例如查询订阅状态使用:
text
GET https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/{transactionId}如果 Server 不知道交易属于生产还是沙盒,可以先查生产接口;生产返回 4040010 TransactionIdNotFoundError 时,再查沙盒接口。/inApps 路径大小写敏感,不要写成 /inapps。
TestFlight 规则
TestFlight 包的内购默认走沙盒环境,测试者不会被真实扣款,也不会把购买带到正式 App Store 环境。
TestFlight 的自动续期节奏和普通 Sandbox Apple Account 不同:所有订阅周期都按每天续期一次,最多在一周内续期 6 次。例如 1 month 订阅在 February 1 购买后,会在 February 2 到 February 7 每天续期一次,然后在 February 8 关闭自动续期。
要在 TestFlight 里测试不同续期速度、Billing Retry 或 Billing Grace Period,需要登录 Sandbox Apple Account 并使用沙盒账号设置。Apple 也明确限制:Sandbox Apple Account 只能用于测试自己开发者账号下的 App。
测试矩阵
一轮比较完整的沙盒测试至少覆盖这些用例:
| 用例 | App 端检查 | Server 端检查 |
|---|---|---|
| 商品拉取 | Product.products(for:) 返回完整商品,价格和周期显示正确。 | 不需要。 |
| 首次购买 | .verified 后展示权益,finish() 时机正确。 | JWS 验签通过,状态 active,收到 SUBSCRIBED。 |
| 自动续期 | Transaction.updates 或重新启动后权益仍有效。 | 收到 DID_RENEW,expiresDate 推进。 |
| 关闭自动续期 | 到期前仍有权益。 | 只记录续期关闭,不提前停权;到期后处理 EXPIRED。 |
| 重新订阅 | 过期后再次购买可恢复权益。 | 新交易与原 originalTransactionId 链路处理正确。 |
| 升级 | 高等级立即生效。 | 旧交易 isUpgraded 不再发低等级权益。 |
| 降级 | 当前周期保持原权益。 | 下一周期商品变更正确。 |
| 账单失败 | 无宽限期时停权,有宽限期时继续服务。 | 状态 3 / 4、gracePeriodExpiresDate 和恢复通知正确。 |
| App 外交易 | App 启动后通过 Transaction.updates 接住交易。 | 通知或 Server API 能查到交易。 |
| 中断购买 | 不提前发权益,用户完成动作后能继续处理交易。 | 只按最终验证成功的交易发权益。 |
| 退款 | currentEntitlements 移除权益。 | REFUND 后写入撤销时间并停权。 |
| 通知重放 | 重复通知不重复发权益。 | notificationUUID 和 transactionId 幂等。 |
| 清空历史 | 首购、试用资格、重新购买能重新测试。 | 沙盒数据不要污染生产数据。 |
实现清单
落地时可以按下面几项检查:
- 订阅商品、等级和业务会员等级有一张明确映射表。
- App 购买时传入
appAccountToken,Server 能反查用户。 - App 启动即监听
Transaction.updates。 - App 用
Transaction.currentEntitlements刷新本地权益。 - Server 验证所有来自 App、Server API、Server Notifications 的 JWS。
- Server 按
notificationUUID、transactionId做幂等。 - Server 用
Get All Subscription Statuses做最终状态确认。 - 生产和沙盒数据分库、分表或至少用
environment强隔离。 - webhook 成功入库后再返回
2xx。 - 定期用 Server API 补偿活跃订阅状态,避免漏通知造成权益漂移。
参考
- Auto-renewable Subscriptions
- Offer auto-renewable subscriptions
- Auto-renewable subscription information
- In-App Purchase
- Transaction
- Transaction.currentEntitlements
- Transaction.updates
- App Store Server API
- App Store Server API changelog
- App Store Server Notifications
- App Store Server Notifications V2
- Enter server URLs for App Store Server Notifications
- Simplifying your implementation by using the App Store Server Library
- Testing at all stages of development with Xcode and the sandbox
- Testing In-App Purchases with sandbox
- Create a Sandbox Apple Account
- Manage Sandbox Apple Account settings
- Testing purchases made outside your app
- Testing failing subscription renewals and In-App Purchases
- Testing refund requests
- Testing Ask to Buy in Xcode
- Testing subscriptions and In-App Purchases in TestFlight
- Enable Billing Grace Period for auto-renewable subscriptions
- Responding to App Store Server Notifications
- Get All Subscription Statuses
