20

【WWDC22 10039】Xcode StoreKit 测试的新功能

 1 year ago
source link: https://xiaozhuanlan.com/topic/5842093617
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【WWDC22 10039】Xcode StoreKit 测试的新功能 - 小专栏

814a1ae840e4ad189422e7560e4c85ac
【WWDC22 10039】Xcode StoreKit 测试的新功能
预计阅读时间58分钟 5 月前

摘要:本文主要聚焦于 In-App Purchase 内购商品的测试。在 Xcode 12 之前,App 内购买项目是不能在 Xcode 模拟器中进行购买,只能使用真机进行测试内购充值,因为模拟器无法连接到 App Store 服务器进行交易。苹果在 WWDC20 推出了 StoreKit Testing,通过 Xcode 12 创建 StoreKit 配置文件和搭建本地测试环境,实现本地 App 内购买和验证收据等测试流程,而无需依赖 App Store 服务器。而今年的 WDC22 苹果对 StoreKit 测试流程改进完善,包含 Xcode 14 中测试功能的优化,支持订阅商品更多场景的测试,还有 StoreKit 配置文件通过 App Store Connect 自动同步等等。

基于 Session 10039 梳理

作者:iHTCboy,目前就职于三七互娱37手游,从事游戏 SDK 开发多年,对 IAP 和 SDK 架构设计有丰富的实践经验。

审核:
黄骋志(橙汁),老司机技术社区核心成员,现于西瓜视频负责稳定性 OOM/Watchdog 相关工作。

SeaHub,目前任职于腾讯 TEG 计费平台部,负责搭建服务于腾讯系业务的支付组件 SDK,对 IAP 相关内容及 SDK 设计开发有一定的经验。

Xcode-StoreKit-Testing-00
Xcode-StoreKit-Testing-00

在 Xcode 12 之前,App 内购买项目是不能在 Xcode 模拟器中进行购买,只能使用真机进行测试内购充值,因为模拟器无法连接到 App Store 服务器进行交易。苹果在 WWDC20 推出了 StoreKit Testing,通过 Xcode 12 创建 StoreKit 配置文件和搭建本地测试环境,实现本地 App 内购买和验证收据等测试流程,而无需依赖 App Store 服务器。而今年的 WDC22 苹果对 StoreKit 测试流程改进完善,包含 Xcode 14 中测试功能的优化,支持订阅商品更多场景的测试,还有 StoreKit 配置文件通过 App Store Connect 自动同步等等。

Xcode-StoreKit-Testing-01
Xcode-StoreKit-Testing-01

2、回顾 StoreKit Testing 功能

2.1 StoreKit App 内购买的测试方式

在讲解 StoreKit 测试的新功能之前,小编先带大家回顾一下 StoreKit 测试的历史流程,这样我们才能理解这个新功能的改进的意义。在苹果文档 Original API for In-App Purchase 中有这样的一张图:

Xcode-StoreKit-Testing-02
Xcode-StoreKit-Testing-02

从这张图可以看出 StoreKit API 的测试必须依赖四方:

  • 开发者 app
  • 开发者服务器
  • StoreKit
  • App Store 服务器

这样互相循环依赖的关系,导致开发者需要测试 StoreKit 功能就非常的被动,主要的问题是依赖 App Store 服务器,一方面是 StoreKit 内购买需要通过 App Store 服务器创建交易(transaction),另一方面是开发者需要通过 App Store 服务器来校验票据(receipt)。而在 Xcode 和模拟器中,StoreKit 并不支持 App Store 服务器交互,导致无法完成流程的闭环。

Xcode-StoreKit-Testing-03
Xcode-StoreKit-Testing-03

所以,要测试 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 后缀的文件,点击左下角的 + 可以选择创建商品类型,根据需要填写要测试的商品信息。

Xcode-StoreKit-Testing-04
Xcode-StoreKit-Testing-04

要启动 StoreKit Testing 功能,需要在项目的 Edit Scheme 中切换到 Run 栏中的 Options 标签,再在 StoreKit Configuration 中选中需要测试的 StoreKit 配置文件即可。然后运行项目后,在 Xcode 的调试栏中点击 Manage StoreKit Transactions 图标,可以打开订单交易管理界面,可以对交易的任一订单进行删除、退款、中断、同意或拒绝购买等操作。

Xcode-StoreKit-Testing-05
Xcode-StoreKit-Testing-05

Xcode 本地创建和生成的交易订单的票据是使用单独的 RSA 密钥生成,使用 PKCS7 填充算法,公钥可以在 Xcode 的 Editor 菜单栏中 Save Public Certificate 导出。

Xcode-StoreKit-Testing-06
Xcode-StoreKit-Testing-06

另外,苹果推出了 StoreKitTest Framework 用于在 Xcode 中编写单元测试和持续集成测试,以实现 StoreKit 自动化测试。简单的一个测试用例如下:

import XCTest
import StoreKitTest
class InAppPurchaseTests: XCTestCase {
private let store = Store()
private var testSession: SKTestSession!
func testRecipeUnlock() throws {
let session = try SKTestSession(configurationFileNamed: "NonConsumable")
session.disableDialogs = true
session.clearTransactions()
buyProduct(Smoothie.thatsBerryBananas.productID)
let contentAvailable = store.receiptContains(productIdentifier: Smoothie.thatsBerryBananas.productID)
XCTAssertTrue(contentAvailable, "Expected content for \(Smoothie.thatsBerryBananas.productID) is not available")

最后,关于 WWDC20 StoreKit Testing 的详细介绍,可以参考我们之前的文章 WWDC20 - 介绍 Xcode 中的 StoreKit 测试

3、Xcode 14 中 StoreKit Testing 新功能

目前 Xcode 中 StoreKit 测试新流程如下图:

Xcode-StoreKit-Testing-07
Xcode-StoreKit-Testing-07

开发者可以串联 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 拉取已经填好参数的配置文件,同时配置文件也可以进行更新,具体看下文介绍。

Xcode-StoreKit-Testing-08
Xcode-StoreKit-Testing-08

这里选择的 App 主要目的是确认同步那个 App 的商品信息,不需要与当前项目的 App(Bundle ID)一致,也能同步商品信息下来。

App Store Connect 同步的 StoreKit Configuration 文件,只能点击刷新按钮同步最新的商品更新信息,而不能在 Xcode 中修改。如果需要本地修改,可以选择配置文件后,点击 Xcode 的 Editor 菜单栏中 Convert to Local StoreKit Configuration 转换成本地配置文件,转换成功后将不能在从 App Store Connect 中同步了。

Xcode-StoreKit-Testing-09
Xcode-StoreKit-Testing-09

如果转换成本地配置文件,Xcode 会有一个警告提醒,点击 Conver File 才能转换成功。另外,如果不想转换,可以点击某个商品的配置,然后复制粘贴到其它的本地配置文件中,这里就不在赘述。

同步文件的区别

其实 StoreKit Configuration 是一个 json 格式的配置文件。

未勾选同步时,创建的 StoreKit 配置文件内容:

"identifier" : "EDA3AE34",
"nonRenewingSubscriptions" : [
"products" : [
"settings" : {
"subscriptionGroups" : [
"version" : {
"major" : 2,
"minor" : 0

创建同步的 StoreKit 配置文件(未点击同步前)的内容:

"identifier" : "9F120B79",
"nonRenewingSubscriptions" : [
"products" : [
"settings" : {
"_applicationInternalID" : "914453386",
"_developerTeamID" : "28PV6G9666"
"subscriptionGroups" : [
"version" : {
"major" : 2,
"minor" : 0

从内容上可以猜到,identifier 表示配置文件的唯一标识,_applicationInternalID 表示 app id,而 _developerTeamID 表示开发者的团队唯一标识。

苹果是用 settings 字段里的 _applicationInternalID_developerTeamID 这两个键值共同来判断是本地的还是同步的配置文件:

"settings" : {
"_applicationInternalID" : "914453386",
"_developerTeamID" : "28PV6G9666"

直接修改本地的配置文件,可以实现同步 AppStore Connect 的配置。但是需要注意,本地配置的商品信息会被删除,覆盖来 AppStore Connect 配置的商品信息。

一个同步后的 StoreKit 配置文件的内容示例:

"identifier" : "02ECE2F0",
"nonRenewingSubscriptions" : [
"products" : [
"displayPrice" : "0.99",
"familyShareable" : false,
"internalID" : "1545378276",
"localizations" : [
"description" : "加餐:给开发者打赏一个鸡腿",
"displayName" : "1个鸡腿",
"locale" : "zh_CN"
"productID" : "com.iHTCboy.chicken",
"referenceName" : "一个鸡腿",
"type" : "Consumable"
"displayPrice" : "1.99",
"familyShareable" : false,
"internalID" : "1545827693",
"localizations" : [
"description" : "加餐:给开发者打赏一杯咖啡",
"displayName" : "一杯咖啡",
"locale" : "zh_CN"
"productID" : "com.iHTCboy.coffee",
"referenceName" : "一杯咖啡",
"type" : "Consumable"
"settings" : {
"_applicationInternalID" : "914453386",
"_developerTeamID" : "28PV6G9666",
"_lastSynchronizedDate" : 679737877.04214501
"subscriptionGroups" : [
"id" : "20919269",
"localizations" : [
"name" : "VIP",
"subscriptions" : [
"adHocOffers" : [
"codeOffers" : [
"displayPrice" : "1.99",
"familyShareable" : false,
"groupNumber" : 1,
"internalID" : "1606645471",
"introductoryOffer" : null,
"localizations" : [
"productID" : "com.iHTCboy.month",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "高级月卡",
"subscriptionGroupID" : "20919269",
"type" : "RecurringSubscription"
"version" : {
"major" : 2,
"minor" : 0

同步的 StoreKit 配置文件与不同步的 StoreKit 配置文件的商品内容格式是一样的,这里就不在赘述。

3.2 本地订单交易管理器(the transaction manager)

之前的 Xcode 订单交易管理界面,可以对交易的任一订单进行删除、退款、中断、同意或拒绝购买等操作。

Xcode-StoreKit-Testing-10
Xcode-StoreKit-Testing-10

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 里申请退款的接口:

Xcode-StoreKit-Testing-11
Xcode-StoreKit-Testing-11

在 SwiftUI 中使用 refundRequestSheet(for:isPresented:onDismiss:) 接口实现退款的示例:

struct RefundView: View {
@State private var selectedTransactionID: UInt64?
@State private var refundSheetIsPresented = false
@Environment(\.dismiss) private var dismiss
var body: some View {
Button {
refundSheetIsPresented = true
} label: {
Text("Request a refund")
.bold()
.padding(.vertical, 5)
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
.padding([.horizontal, .bottom])
.disabled(selectedTransactionID == nil)
.refundRequestSheet(
for: selectedTransactionID ?? 0,
isPresented: $refundSheetIsPresented
) { result in
if case .success(.success) = result {
dismiss()

而在本地订单交易管理器,也可以点击 Refund Purchases 图标进行退款。

Xcode-StoreKit-Testing-12
Xcode-StoreKit-Testing-12

那么退款成功后,开发者需要处理退款后的 app 的业务逻辑测试,在 StoreKit 2 中,使用 Transaction.updates 监听所有交易的更新,更新交易的 revocationReason 字段是一个结构体,其中 .developerIssue.other 与上上图中可选择的退款原因是相对应的,所以开发者很容易对这两个撤销原因进行测试。

for await update in Transaction.updates {
let transaction = try update.payloadValue
if let revocationDate = transaction.revocationDate,
let revocationReason = transaction.revocationReason {
print("\(transaction.productID) revoked on \(revocationDate)")
switch revocationReason {
case .developerIssue: <#Handle developer issue#>
case .other: <#Handle other issue#>
default: <#Handle unknown reason#>
<#Revoke access to the product#>
<#...#>

最后,退款测试的环境要求如下:

Xcode Sandbox
iOS and iPadOS 15.2 15.0
macOS 12.1 12.0

4.2 优惠代码测试(Offer codes)

关于 Offer codes(优惠代码)我们这里就略过了,读者可以查看苹果文档了解 优惠代码


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK