本文是《WWDC22 内参》参与创作者,首发于 【WWDC22 10039】Xcode StoreKit 测试的新功能 - 小专栏。
基于 Session 10039 梳理
作者:iHTCboy,目前就职于三七互娱37手游,从事游戏 SDK 开发多年,对 IAP 和 SDK 架构设计有丰富的实践经验。
审核:
黄骋志(橙汁),老司机技术社区核心成员,现于西瓜视频负责稳定性 OOM/Watchdog 相关工作。SeaHub,目前任职于腾讯 TEG 计费平台部,负责搭建服务于腾讯系业务的支付组件 SDK,对 IAP 相关内容及 SDK 设计开发有一定的经验。
王浙剑(Damonwong),老司机技术社区负责人、WWDC22 内参主理人,目前就职于阿里巴巴。
1、前言
在 Xcode 12 之前,App 内购买项目是不能在 Xcode 模拟器中进行购买,只能使用真机进行测试内购充值,因为模拟器无法连接到 App Store 服务器进行交易。苹果在 WWDC20 推出了 StoreKit Testing,通过 Xcode 12 创建 StoreKit 配置文件和搭建本地测试环境,实现本地 App 内购买和验证收据等测试流程,而无需依赖 App Store 服务器。而今年的 WDC22 苹果对 StoreKit 测试流程改进完善,包含 Xcode 14 中测试功能的优化,支持订阅商品更多场景的测试,还有 StoreKit 配置文件通过 App Store Connect 自动同步等等。
2、回顾 StoreKit Testing 功能
2.1 StoreKit App 内购买的测试方式
在讲解 StoreKit 测试的新功能之前,小编先带大家回顾一下 StoreKit 测试的历史流程,这样我们才能理解这个新功能的改进的意义。在苹果文档 Original API for In-App Purchase 中有这样的一张图:
从这张图可以看出 StoreKit API 的测试必须依赖四方:
- 开发者 app
- 开发者服务器
- StoreKit
- App Store 服务器
这样互相循环依赖的关系,导致开发者需要测试 StoreKit 功能就非常的被动,主要的问题是依赖 App Store 服务器,一方面是 StoreKit 内购买需要通过 App Store 服务器创建交易(transaction),另一方面是开发者需要通过 App Store 服务器来校验票据(receipt)。而在 Xcode 和模拟器中,StoreKit 并不支持 App Store 服务器交互,导致无法完成流程的闭环。
所以,要测试 App 内购买功能有以下三种测试环境:
Production
:生产环境,也就是 App Store 下载的 app,需要使用真钱才能进行测试。Sandbox
:沙盒环境,开发者用 Development 或 Ad Hoc 证书打包调试时, 在真机中可以进行 App 内购买测试,但需要登录沙盒测试账号。TestFlight
:测试环境,面向外部测试员,App 内购买项目使用的是沙盒环境,但不需要测试员登录沙盒测试账号。
2.2 Xcode 中 StoreKit Testing 功能
苹果在 WWDC20 推出了 StoreKit Testing,它的目的是脱离 App Store 服务器,让开发者本地就能完成 App 内购买流程。原理是,Xcode 中创建一个 StoreKit Configuration File
本地配置文件,Xcode 通过这个配置文件模拟 StoreKit 与 App Store 服务器的交互流程,从而实现在 Xcode 模拟器中发起 App 内购买操作。
具体的操作是在 Xcode 项目中新建文件,选择创建 StoreKit Configuration File
,然后选中生成 .storekit
后缀的文件,点击左下角的 +
可以选择创建商品类型,根据需要填写要测试的商品信息。
要启动 StoreKit Testing 功能,需要在项目的 Edit Scheme
中切换到 Run
栏中的 Options
标签,再在 StoreKit Configuration
中选中需要测试的 StoreKit 配置文件即可。然后运行项目后,在 Xcode 的调试栏中点击 Manage StoreKit Transactions
图标,可以打开订单交易管理界面,可以对交易的任一订单进行删除、退款、中断、同意或拒绝购买等操作。
Xcode 本地创建和生成的交易订单的票据是使用单独的 RSA 密钥生成,使用 PKCS7
填充算法,公钥可以在 Xcode 的 Editor
菜单栏中 Save Public Certificate
导出。
另外,苹果推出了 StoreKitTest Framework 用于在 Xcode 中编写单元测试和持续集成测试,以实现 StoreKit 自动化测试。简单的一个测试用例如下:
1 | import XCTest |
最后,关于 WWDC20 StoreKit Testing 的详细介绍,可以参考我们之前的文章 WWDC20 - 介绍 Xcode 中的 StoreKit 测试。
3、Xcode 14 中 StoreKit Testing 新功能
目前 Xcode 中 StoreKit 测试新流程如下图:
开发者可以串联 Xcode、App Store Connect、TestFlight、App Store 实现完整的流程,StoreKit 在沙盒和 Xcode 中的测试。
3.1 支持 StoreKit 的配置文件从 App Store Connect 同步
我们上面讲到的 StoreKit Configuration 文件的创建和配置,其实是比较麻烦的,因为开发者在 App Store Connect 创建 App 之后需要创建配置 App 内购买商品,然后在 Xcode 中创建 StoreKit Configuration 文件后,还需要把全部的商品信息再配置一次,对于开发者来说是非常麻烦的重复事情。
在 Xcode 14 中,苹果解决了 StoreKit Configuration 文件需要手动配置的商品的问题,开发者在创建时 StoreKit Configuration 文件时,可以选择勾选 Sync this file with an app in App Store Connect
,然后选择开发者团队和 App ,就可以从 AppStore Connect 拉取已经填好参数的配置文件,同时配置文件也可以进行更新,具体看下文介绍。
这里选择的 App 主要目的是确认同步那个 App 的商品信息,不需要与当前项目的 App(Bundle ID)一致,也能同步商品信息下来。
App Store Connect 同步的 StoreKit Configuration 文件,只能点击刷新按钮同步最新的商品更新信息,而不能在 Xcode 中修改。如果需要本地修改,可以选择配置文件后,点击 Xcode 的 Editor
菜单栏中 Convert to Local StoreKit Configuration
转换成本地配置文件,转换成功后将不能在从 App Store Connect 中同步了。
如果转换成本地配置文件,Xcode 会有一个警告提醒,点击 Conver File
才能转换成功。另外,如果不想转换,可以点击某个商品的配置,然后复制粘贴到其它的本地配置文件中,这里就不在赘述。
同步文件的区别
其实 StoreKit Configuration 是一个 json
格式的配置文件。
未勾选同步时,创建的 StoreKit 配置文件内容:
1 | { |
创建同步的 StoreKit 配置文件(未点击同步前)的内容:
1 | { |
从内容上可以猜到,
identifier
表示配置文件的唯一标识,_applicationInternalID
表示app id
,而_developerTeamID
表示开发者的团队唯一标识。
苹果是用 settings
字段里的 _applicationInternalID
和 _developerTeamID
这两个键值共同来判断是本地的还是同步的配置文件:
1 | "settings" : { |
直接修改本地的配置文件,可以实现同步 AppStore Connect 的配置。但是需要注意,本地配置的商品信息会被删除,覆盖来 AppStore Connect 配置的商品信息。
一个同步后的 StoreKit 配置文件的内容示例:
1 | { |
同步的 StoreKit 配置文件与不同步的 StoreKit 配置文件的商品内容格式是一样的,这里就不在赘述。
3.2 本地订单交易管理器(the transaction manager)
之前的 Xcode 订单交易管理界面,可以对交易的任一订单进行删除、退款、中断、同意或拒绝购买等操作。
Xcode 14 中,点击某个交易订单,可以看到右侧栏会显示商品的交易详细信息,如果是订阅商品还包含订阅过期时间、续订时间等等。另外底部新增搜索栏,可以搜索商品的 ID 或交易时间等。
4、StoreKit Testing 的改进案例
那么如何结合 Xcode 进行 StoreKit 测试,如果之前大家没有尝试过,看看这几个案例就能大概学会啦。
- 退款测试(Refund requests)
- 优惠代码测试(Offer codes)
- 订阅涨价测试(Price increases)
- 扣费重试和宽限期(Billing retry and grace period)
4.1 退款测试(Refund requests)
在 iOS 15 苹果提供了 app 里申请退款的接口:
在 SwiftUI 中使用 refundRequestSheet(for:isPresented:onDismiss:) 接口实现退款的示例:
1 | struct RefundView: View { |
而在本地订单交易管理器,也可以点击 Refund Purchases
图标进行退款。
那么退款成功后,开发者需要处理退款后的 app 的业务逻辑测试,在 StoreKit 2 中,使用 Transaction.updates
监听所有交易的更新,更新交易的 revocationReason 字段是一个结构体,其中 .developerIssue
和 .other
与上上图中可选择的退款原因是相对应的,所以开发者很容易对这两个撤销原因进行测试。
1 | for await update in Transaction.updates { |
最后,退款测试的环境要求如下:
Xcode | Sandbox | |
---|---|---|
iOS and iPadOS | 15.2 | 15.0 |
macOS | 12.1 | 12.0 |
4.2 优惠代码测试(Offer codes)
关于 Offer codes
(优惠代码)我们这里就略过了,读者可以查看苹果文档了解 优惠代码。
优惠代码测试的测试,首先是在 StoreKit 配置文件的订阅商品中点击添加 Offer Codes 栏的 +
进行配置:
在 iOS 14 中就增加 presentCodeRedemptionSheet() 接口,实现 app 内兑换优惠代码:
而在 SwiftUI 中需要 iOS 16+ 中通过 offerCodeRedemption(isPresented:onCompletion:) 接口实现 App 内兑换优惠代码的示例:
1 | struct SubscriptionPurchaseView: View { |
兑换优惠代码成功的交易,可以在本地订单交易管理器,交易订单的右侧栏 Renewals
标签中,看到订阅更新的信息。
那么开发者需要处理兑换优惠代码后的 app 的业务逻辑测试,在 StoreKit 2 中,使用 Transaction.updates
和 Product.SubscriptionInfo.Status.updates
监听所有订阅商品交易的状态更新:
1 | for await verificationResult in Transaction.updates { |
其中 Product.SubscriptionInfo.Status.updates
接口中返回的交易字段 offerType
可以确认订阅的优惠类型,关于 OfferType 优惠类型可以阅读文档:优惠类型。
1 | for await status in Product.SubscriptionInfo.Status.updates { |
优惠代码(Offer Codes)测试的 StoreKit 配置在 Xcode 13.3+ 以上就可以设置,所以搭配的测试的设备系统必须是 iOS 15.4 或 iPadOS 15.4 以上。
4.3 订阅涨价测试(Price increases)
随着 App 订阅的流行,很多 App 的订阅可能因为业务或者服务器成本等原因,提高订阅价格。而部分订阅涨价需要用户同意才能续订,今年 WWDC22 苹果推出了 StoreKit Message 接口,开发者可以在 app 内显示涨价提示:
关于 StoreKit Message 接口介绍,可以参考 WWDC22 - 探索 In-App Purchase 新特性。StoreKit Message 接口代码示例:
1 | private var pendingMessages: [Message] = [] |
关于提高自动续期订阅价格可以参考文档:
那么,要怎么提高自动续期订阅价格,在本地订单交易管理器可以点击图标或者右键点击 Request Price Increase Consent
按钮,这样就表示这个商品要提高下一个订阅周期的订阅价格:
因为除了利用 StoreKit Message 提示弹窗中点击同意涨价外,用户也可能会通过电子邮件等其它方式对价格上涨做出选择,所以为了模拟这个场景,可以在本地订单交易管理器,点击 Approve
(批准)和 Decline
(拒绝)按钮来模拟用户的选择。
最后,可以通过 priceIncreaseStatus
判断用户是否同意涨价,通过 expirationReason
字段的 .didNotConsentToPriceIncrease
类型判断用户没有同意涨价。在 StoreKit 2 和 iOS 15 实现的代码示例:
1 | for await status in Product.SubscriptionInfo.Status.updates { |
使用 iOS 15.4 可以在 StoreKitTest Framework 中编写单元测试代码进行涨价测试:
1 | let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>") |
订阅涨价测试支持在 Xcode 13.3 以上使用:
Status | Message | |
---|---|---|
iOS and iPadOS | 15.4 | 有 |
macOS | 12.3 | 无 |
watchOS | 8.5 | 无 |
tvOS | 15.4 | 无 |
注:其中 StoreKit Message 功能只有 iOS 16 或 iPadOS 16 以上支持,其它系统暂不支持。
4.4 扣费重试和宽限期(Billing retry and grace period)
扣费重试一般是出现在玩家的银行卡信息过期或者绑定的支付方式过期或撤销等,导致订阅无法按时续期,系统会进行扣费重试。如果重试扣费成功,或者用户续费,则订阅续订成功:
默认情况下扣费重试阶段,用户的订阅服务已经过期,导致用户无法使用服务。为了解决扣费重试阶段,用户还能享受订阅服务,苹果提供了一个过渡阶段:Grace period
(宽限期),在宽限期内订阅服务可继续享受。
要实现扣费重试和宽限期的测试,可以在 Xcode 中分别选择 Enable Billing Retry on Renewal
和 Enable Billing Grace Period
:
注:为了加速订阅过期时间,可以在
Subscription Renewal Rate
选择更快的订阅过期时间,这样更快的进入模拟的测试阶段。
最后,要模拟解决扣费失败后成功的场景,可以在本地订单交易管理器中,点击 Resolve Issues
表示解决扣费问题,让扣费成功后进入到下一个订阅周期中。
在代码逻辑中处理,gracePeriodExpirationDate
字段的时间小于当前时间,就表示订阅在宽限期内,允许用户继续享受订阅服务。而 isInBillingRetry
字段,则表示扣费重试阶段。
1 | for await status in Product.SubscriptionInfo.Status.updates { |
在订阅扣费重试阶段,可以提示或引导用户解决扣费失败的问题。具体来说,可以引导用户打开链接 https://apps.apple.com/account/billing
会跳转到 App Store 用户账号的 管理付款方式
,从而解决问题。
1 | struct SubscriptionStatusView: View { |
使用 iOS 15.4 以上可以用 StoreKitTest Framework 中编写单元测试代码实现扣费重试测试:
1 | let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>") |
订阅扣费重试测试支持在 Xcode 13.3 以上使用:
Xcode 13.3 | Sandbox | |
---|---|---|
iOS and iPadOS | 15.4 | 16.0 |
macOS | 12.3 | 无 |
watchOS | 8.5 | 无 |
tvOS | 15.4 | 无 |
Server | N/A | 有 |
注:订阅扣费重试,Sandbox 环境下,苹果会发送 App Store Server Notifications 订阅状态变更的通知,如
DID_FAIL_TO_RENEW
/EXPIRED
/BILLING_RETRY
。而 Xcode 环境测试则不会有通知。
5、沙盒测试环境的更新
在 iOS 12 之前沙盒账号测试内购买需要先登出 App Store 账号,在 iOS 12 开始,苹果才提供了单独的沙盒测试账号入口:
5.1 沙盒 Apple ID 创建
开发人员可以更轻松地创建沙盒账号,相比以前少了 安全提示问题
、安全提示问题答案
、出生日期
三个选项。另外,密码强度不满足时会有提示语。
5.2 App Store Connect API
App Store Connect API 新支持:
- 查询沙盒账号
- 清除沙盒账号的内购买历史记录
- 设置沙盒账号的购买中断状态
5.3 账单扣费失败模拟(Billing failure simulation)
简单来说,沙盒测试账号中增加了 Allow Purchase & Renewals
开关,用于测试订阅到期自动扣费和失败重试。
比如当关闭这个按钮时,表示自动续订失败,订阅状态会进入扣费重试和宽限期中,此时就可以在沙盒环境中测试。
如果是 App Store Server Notifications V2 会收到 DID_FAIL_TO_RENEW
GRACE_PREIOD
宽限期的通知:
开发者也可能通过 App Store Server API 主动查询订阅状态:
如果是 Original StoreKit API,则通过苹果票据验证接口获取状态:
6、总结
关于 StoreKit 和 In-App Purchase 测试,一般开发者会更加关注在 Sandbox(沙盒环境)下测试 App 内购买功能,因为这与 Production(生产环境)的区别最小,但是测试流程比较麻烦,需要开发者证书、沙盒测试账号登录等,开发者账号还必须成功绑定银行卡信息后才能调试内购买功能。如果是订阅类型的商品,还需要覆盖测试的场景非常多,测试起来更加的麻烦。
从上文的案例和代码示例可以知道,StoreKit Testing 借助 SwiftUI 和 StoreKit 2,让测试流程的实现技术更加自然。一方面是 SwiftUI 可以快速构建 UI 界面,更容易实现商品页面的展示和调整;另一方面 StoreKit 2 的 JWS transaction 票据不需要通过苹果的 StoreKit 服务器验证,更方便实现票据的校验流程。所以,对于新项目,或者使用 StoreKit 2 改造内购买逻辑流程时,使用 StoreKit Testing 将会大大提高代码测试的效率。
所以基于 Xcode 的 StoreKit Testing 和 StoreKitTest Framework 框架,开发者有了更加高效的测试方式。而今年 WWDC22 改进后更加方便和高效,开发者无需关注证书配置和沙盒环境账号等,就能实现本地的内购买测试,对于需要验证某个商品购买逻辑完备性,或 App 新增加内购买功能时,只需要闭环本地的代码逻辑而无需验证票据等,StoreKit 本地测试会更加顺畅,建议读者可以尝试使用!
7、参考链接
- What’s new in StoreKit testing - WWDC22
- Introducing StoreKit Testing in Xcode - WWDC20
- WWDC20 10659 - 介绍 Xcode 中的 StoreKit 测试 - 小专栏
- 聚焦探索 In-App Purchase 新特性
- 管理自动续期订阅的定价 - App Store Connect 帮助
- 订阅通知更新 - 最新动态 - Apple Developer
- refundRequestSheet(for:isPresented:onDismiss:) | Apple Developer Documentation
- Transaction.RevocationReason | Apple Developer Documentation
- presentCodeRedemptionSheet() | Apple Developer Documentation
- offerCodeRedemption(isPresented:onCompletion:) | Apple Developer Documentation
- Product.SubscriptionInfo.Status | Apple Developer Documentation
- StoreKit Test | Apple Developer Documentation
- Testing in-app purchases in Xcode | Apple Developer Documentation
- Setting Up StoreKit Testing in Xcode | Apple Developer Documentation
- Testing at All Stages of Development with Xcode and Sandbox | Apple Developer Documentation
- Testing In-App Purchases with Sandbox | Apple Developer Documentation
- 测试 App 内购买项目 - App Store Connect 帮助
- 使用沙盒测试 App 内购买项目 - Apple Developer
- 为自动续期订阅设置优惠代码 - App Store Connect 帮助
- 提供自动续期订阅 - App Store Connect 帮助