大多数浏览器和
Developer App 均支持流媒体播放。
-
StoreKit 测试的新功能
发现可帮助您测试 App 内购买项目与订阅的最新工具。我们将向您介绍如何在 Xcode 中对 App Store Connect 的产品进行 StoreKit 测试,了解交易管理器的改进,并探索 Xcode Previews 中的 App 内购买流程。我们还将与您分享为沙盒环境设置 Apple ID 的最佳实践,说明如何为退款请求、价格上调同意状态、计费重试等创建测试。
资源
- Auto-renewable subscriptions overview
- Handling Subscriptions Billing
- Implementing offer codes in your app
- Learn more about setting up offer codes
- Reducing Involuntary Subscriber Churn
- Setting up StoreKit Testing in Xcode
- Testing In-App Purchases with sandbox
相关视频
WWDC23
WWDC22
WWDC21
WWDC20
-
下载
♪ ♪ Greg: 大家好 我是Greg 欢迎使用 StoreKit 测试中的新功能 在本次课程中 Peter 和我将 重点强调一些很棒的新功能 可用于 StoreKit 中的 App 内购买项目的测试 我们先来看看使用 Xcode 14 精简你的 App 内购买项目测试的一些方法 接下来 我们来看一些全新的功能 你可以利用这些功能 在你的 App 内订阅项目实现中 覆盖更多的角落案例 最后 Peter 将展示沙盒测试环境 新的加强功能 我们将与 Food Truck 合作 这是一款为销售甜甜圈的 Food Truck 运营商提供强大功能的 App 我已经加入了 StoreKit 以提供 完整版的 Food Truck 销售历史记录功能 以及订阅加强版的 Social Feed 服务 在整个课程中 我们将在 Xcode 中使用 StoreKit 来测试我们的 App 的 App 内购买项目功能 回到 WWDC 2020 我们在 Xcode 中 引入了 StoreKit 测试 使你能够在 Xcode 中开始测试 你的 App 内购买项目 今年 有了 Xcode 14 我们很高兴能够分享一些 StoreKit app 的测试生命周期的更新 像之前一样 你可以创建 Xcode 中的 StoreKit 配置文件 并开始在无需在 App Store Connect 中 设置 App 的情况下 测试你的 App 内购买项目实现 当你准备好配置你在 App Store Connect 中的 App 时 我们向你推荐 Xcode 14 中的一项全新功能 它可以让你使用与你已经在 Xcode 中利用 StoreKit 测试 进入 App Store Connect 时一样的 App 内购买项目产品 如果你已经有 App 在商店中 你现在就可以在 Xcode 中 使用 StoreKit 测试 无需从头开始设置 StoreKit 配置文件 这个方便的功能让你可以 配置一次你的 App 内购买项目 并在 Xcode 中本地使用相同的配置 在你的单元测试中 在沙盒环境中 以及在 App Store 准备好发布时 在 App Store Connect 中使用 Xcode 同步你的产品很容易 首先 在 App Store Connect 中 配置你的产品 例如这个 Social Feed+ 订阅 然后 你创建一个同步的 Xcode 中的配置文件 这会将产品数据加载到 Xcode 里面 如果你想做出改变 例如更新美国英文标题 你可以在 App Store Connect 中 做出改变 并再次在 Xcode 中同步你的配置 你还可以将你已经同步的配置 转换到一个本地的 可编辑的文件中 以进行即时更改 转换同步配置到本地配置 是一种单向操作 要想再次同步 你就需要创建一个新的配置文件 我已经开始设置 Social Feed+ 的订阅组 这是 Food Truck app 提供的 加强版 Social Feed 服务 让我们跳入 Xcode 看看 在 Xcode 中使用 StoreKit 测试时 如何使用这些产品 我在我的 Mac 上打开了 Food Truck 项目 首先 我们将通过转到文件菜单 来创建一个新的 StoreKit 配置文件 制作一个新文件 通过 StoreKit 过滤 并单击下一步
在 Xcode 14 中 当我们创建 一个新的配置文件时 我们得到这个复选框来利用 App Store Connect 中的 App 保持文件同步 创建本地文件时 要填写名称 并取消选中该框 设置同步时 我们只需要选中该框 并确认正确的组和 App 被选中 如果需要 我们可以选择不同的小组 以及使用选择器菜单的 App 我们点击下一步 并选择一个地方来保存我们的文件 一旦我们保存了文件 App 内购买项目元数据 便开始从 App Store Connect 同步 在下载数据时 我们可以继续开发我们的 App 并在活动栏中跟踪其进度 同步完成后 你会注意到这个文件看起来 与典型的 StoreKit 配置文件有所不同 那是因为同步的文件处于只读状态 我们一眼便可以看到 Xcode 中 所有的数据 但要做出改变 我们就必须打开 App Store Connect
在 Safari 中 我有 Social Feed+ 月度产品 让我们通过添加后缀 来为这款产品更新英文标题 帮助将产品与年度计划区分开来
现在更新完成 让我们保存 并返回 Xcode
要想让这个改变反映在 我们的配置文件中 我们只需要在左下角 按下这个同步按钮即可
同步完成后 我们可以看到 Xcode 中反映的变化
即使同步的文件是只读的 我们也可以将数据复制到本地文件 在 Xcode 中进行快速更改
除了从配置文件中复制项目之外 我们还可以将整个同步文件 转换到本地的可编辑文件 我们需要做的就是打开 我们同步的文件 进入编辑器菜单 然后点击 Convert to Local StoreKit Configuration
记住 你无法撤消 转换文件后的此项操作 要想再次与 App 同步 你就需要 创建一个新的 StoreKit 配置文件 我想使用 App Store Connect 来保持这个文件同步 所以让我们取消这个警报 现在我们已经同步了文件 让我们配置我们的测试环境 作为开始 我们将打开方案编辑器
选择运行动作 并选择选项
在选项中 我们可以从选择器菜单中 切换不同的 StoreKit 环境 如果我们选择“无” 就会连接到沙盒 如果我们选择 Food Truck 就会连接到 Xcode 环境 根据我们当前的测试需求 来切换环境就是这么简单 现在两个环境都将使用 完全相同的产品 和订阅元数据 让我们暂时选择我们的同步配置文档
我们现在已经在 Xcode 中设置了 StoreKit 所以让我们开始测试吧 由于我们使用的是 SwiftUI app 我们可以在 Xcode 中 预览我们的订阅商店
从 Xcode 14 开始 StoreKit 配置文件中的产品 将直接加载到 SwiftUI 预览中 这使得构建和测试漂亮的 商店用户界面变得超级容易 使用真正的 App 内购买项目数据 让我们通过 为我们的产品添加副标题 来试着将一些细节添加到产品选项 我们只需添加一个 Text 视图 其中包含产品的本地化描述
利用我们在 App Store Connect 中 设置的描述 即可观看预览更新 我认为现在看起来好多了
现在我们的 UI 状态良好 让我们在 iPhone 上运行 App 并开始一些功能测试
在 Xcode 14 中 StoreKit Transaction Manager 中 有一些强大的新工具 随着我们的 App 运行 我们可以 通过按下调试栏中的购买图标 来打开交易管理器
在右边 有一个新的交易检查器 这使我们能够可视化 一笔交易后台的所有细节 这个工具有助于理解 App 内交易项目的状态 例如 我们可以看到日期 对 Social Feed+ 的订阅已过期 以及即将到来的续订信息 我们也可以为了产品 订阅组 或订阅报价 跳转到配置文件 我们只需要点击 订阅组旁边的跳转按钮即可
在配置文件中 我们直接来到了 Social Feed+
当我们查看更高级测试用例时 这个检查器 将在稍后的课程中帮到我们
我们现在也可以过滤我们的交易 这对于浏览包含了 所有这些 Social Feed+ 更新的 交易列表来说真的很有用 在我们的 App 中 你会注意到我们可以 访问年度销售历史记录功能
我们拥有所有这些订阅续订 这使我们很难分辨 哪个交易使我们有权使用该功能 我们可以通过开始输入其 ID 来轻松地为产品找到交易
从自动完成菜单中 选择产品 ID 过滤器
我们还可以在购买日期之前过滤 因此我们可以专注于 我们现在正在进行的购买
由于我们订阅的 Social Feed+已过期 让我们进入 App 并再次订阅
现在我们已经确认了订阅 我们可以看到新交易的出现
我们刚刚通过同步产品和订阅 从 App Store Connect 中 看到了一些在 Xcode 中 加强 App 内购买项目测试的方法 使用你的 Storekit 配置 和 SwiftUI 预览 并在 Transaction Manager 中 利用新工具 现在 我们将继续通过在 Xcode 中使用一些新功能 来测试 Food Truck 的 App 内购买项目功能 以涵盖高级订阅案例 首先 我们来看一下测试退款请求 这使人们能够为在 Food Truck 上 购买的产品申请退款 接下来 我们将测试报价代码 提供针对 Social Feed+ 订阅者的促销 之后 我们来看看 如何在 Food Truck 的用户界面中 处理涨价 最后 通过支持计费重试和宽限期 减少 Social Feed+ 非自愿流失 开始测试退款请求时 我们将导航到我们的 App 中的 这个支持视图 这让我们可以选择最近的交易退款 这种代码很简单 我刚刚为我们的视图添加了一个 refundRequestSheet 视图修饰符 当我们按下退款按钮时 我们将把 isPresented Binding 翻转为真 现在 让我们看看它的实际效果
当 Binding 为真时 退款请求 会出现在我们的视图上方 在 Xcode 环境中测试时 我们选择的问题 与 StoreKit API 中的 RevocationReason 是一一对应的 让我们选择 “Developer Issue” 并按下 “Request Refund ”按钮
在 App Store 中 退款请求 需要一些时间来处理 但是在使用 Xcode 或沙盒进行测试时 退款请求会立即退还交易 在交易管理器中 关于这个更新的交易 我们可以看看检查器 看看我们刚刚选择的撤销原因 和撤销日期
你还可以测试退款 只需在交易管理器中 点击一下退款按钮 退款请求 API 帮助我们 为使用 Food Truck 的人 提供出色的客户支持 现在我们了解了如何测试 Xcode 中的退款请求 让我们看看你用 StoreKit 处理退款交易时可以使用的一些方法
当一笔交易退款后 更新的交易值将 从 Transaction.updates 序列发出 我们可以使用revocationDate 和 revocationReason 属性 来检测这些退款的交易 通过在 Xcode 的退款申请表中 选择相应的选项 很容易测试两个撤销原因案例 这就是你测试 Xcode 中的退款申请表的方式 当使用 Xcode 环境或沙盒时 这适用于 iOS 和 macOS 对于使用 Xcode 进行测试 你需要你的 iPhone 或 iPad 运行 iOS 或 iPadOS 15.2 或更高版本 要想在 Mac 上使用 Xcode 进行测试 你需要 macOS 12.1 或更高版本 现在 让我们来看看 测试订阅报价代码 为此 我们将使用 我们本地的 StoreKit 配置文件 要想为代码提供新的报价 我们就要选择订阅 然后在报价码表下按下 "+" 按钮 然后我们可以配置我们的报价 我们将这个命名为“免费月” 并免费提供一个月
就像在 App Store Connect 中一样 我们选择那些符合条件的客户 以及介绍性报价是否 可以使用此报价兑换 让我们暂时保留默认设置 现在我们的代码已经配置好了 我们按下“完成”按钮 当然 如果你正在同步 使用 App Store Connect 那么你配置的报价将自动显示在 这个表格中 现在我们的报价已经配置好了 我们导航到 App 的商店视图 我已经在视图底部附近 添加了这个按钮 用于兑换订阅报价 如果我们在 Xcode 中 打开商店视图 使用报价代码就和添加一个 offerCodeRedemption 修饰符一样轻松 在我们看来 当有人点击按钮时 isPresent Binding 就会翻转为真 我们看看这是如何工作的
当我们按下按钮时 兑换表出现在我们的 App 上方 在 App Store 中 人们可以输入 在 App Store Connect 中生成的 报价代码 但在 Xcode 中的测试体验更加精简 在我们的配置文件中 我们有所有代码报价的列表 按照它们解锁的订阅进行了分组 为了兑换 让我们点击我们刚刚创建的报价 并按下兑换按钮 付款单出现 我们可以看到代码的报价 将在现收现付介绍性报价之后开始
订阅后 我们会看到一个确认屏幕 我们现在可以关闭工作表 并到 Social Feed+ 验证我们的 App 解锁访问
如果我们看这项新交易的检查器 那么我们就可以看到 介绍性报价目前已应用 由于报价是现收现付 续订部分显示 我们会再续约两次介绍性报价 在这之后 我们刚刚兑换了免费月份代码 然后标准订阅将无限期更新 检查器使我们清楚地了解 我们的订阅状态发生了什么 即使是在复杂的场景中 比如多个报价 我们刚刚看了如何在我们的 本地 StoreKit 配置中的代码配置报价 以及如何在 iPhone 上测试兑换它们 报价代码是提供 灵活促销活动的好方法 对于我们未来和现有的订阅者来说 现在开始在 Food Truck 中使用报价代码 比以往任何时候都容易 现在 让我们来看看如何使用 StoreKit 处理这些报价 兑换代码后 两个 Transaction.updates 和 Status.updates 序列将发出新值 我们可以在交易价值上 检查 offerType 属性 查看是否有报价应用到当前交易 我们刚刚查看了 offerType 值 是否设置了介绍 因为我们允许订阅者用代码报价 兑换介绍性报价 在更新信息值上 我们可以检查 offerType 属性 看看什么样的报价 将在下一次更新中出现 在我们刚刚看到的示例中 我们可以期待初始值为介绍性报价 因为我们使用了现收现付的报价 两个订阅期后 我们将看到值切换到代码 因为我们有一个代码报价堆叠 当 offerType 为代码时 我们可以使用 offerID 属性 来获取申请代码的参考名称 这就是你在 Xcode 中 测试报价代码的方式 从 Xcode 13.3 开始 你可以为代码配置报价 并在 iPhone 和 iPad 上 运行 iOS 15.4 或更高版本来测试它们 现在我们已经验证了 Food Truck 中的代码报价的有效性 让我们测试一下我们的 App 如何处理 Social Feed+ 的涨价 在 Xcode 中测试涨价 真的很简单 首先 我们将提高价格 用于每月的 social feed 订阅
此步骤是可选的 你可以保持价格不变 并且模拟涨价 回到交易管理器 我们需要做的就是选择最新的 订阅交易 并在工具栏中按下 “Request Price Increase Consent”按钮
我们可以在交易管理器中看到 我们的交易现在在 “Price Increase Pending”状态 如果看一下设备 我们就会注意到 我们的 App 上方出现了一张工作表 要求同意涨价 此工作表将自行显示 无需添加任何代码 但我们利用了新的 Messages API 来自定义其行为
让我们看看我们是如何 在代码中使用 Messages API 集成的
我们这里有一个循环迭代的 消息序列 如果我们收到消息 比如涨价 要确保我们没有呈现敏感视图 如甜甜圈编辑器 否则 我们将使用 DisplayMessageAction 来显示消息 如果出现甜甜圈编辑器 我们将保留 Message 值 并在甜甜圈编辑完成之后显示
让我们回到测试
在 App Store 中 现有订阅者 在不同的时间可能会收到 多条涨价的消息 直到他们决定 取消或同意涨价 在 Xcode 中 我们可以完全控制 这些消息什么时候来 当我们每次 在交易管理器中按下按钮时 我们都会再次收到消息 即使交易已经处于涨价状态 现在我们可以测试 我们的延迟逻辑是否确实有效 所以我先打开甜甜圈编辑器
并发送一个再次打开工作表的消息
工作表还没有出现 但是如果我们离开甜甜圈编辑器 工作表会按预期显示 当我们可以接受涨价 或取消工作表中的订阅时 用户事实上可能会通过外部资源 来回应涨价 比如电子邮件 为了对此进行模拟 我们可以在交易管理器中 使用批准和拒绝按钮 由于甜甜圈编辑体验非常好 我将在交易管理器中按下同意按钮 同意新价格 在 Xcode 中使用 StoreKit 测试复杂的极端情况 例如涨价 会很顺利 现在我们来看看如何模拟涨价 让我们看看如何使用 StoreKit 在我们的 App 中处理涨价
在测试涨价状态时 状态更新序列将发出 每次状态变化的新值 我们可以在我们的 App 中 通过检查 priceIncreaseStatus RenewalInfo 值的属性 检测到这些更新 如果客户由于涨价而取消订阅 我们就能够通过检查 在 expireReason 属性中的 didNotConsentToPriceIncrease 来检测到这一点 我们也可以围绕测试涨价 来写单元测试 首先 禁用对话框 可以让我们在 App 没有实际显示 涨价 UI 的情况下允许我们测试 购买订阅后 我们可以使用 requestPriceIncreaseConsentForTransaction API 启动进程 给 ID 传入最新的订阅交易 要验证测试交易是否正在等待涨价 我们将会检查 isPendingPriceIncreaseConsent 属性 最后 根据我们正在测试的内容 我们可以调用 consentToPriceIncreaseForTransaction 或 declinePriceIncreaseForTransaction 来查看我们的 App 如何响应 已完成的涨价案例 这都是为了测试涨价 在所有平台上 涨价都可通过 Xcode 13.3 进行测试 注意 涨价消息 只能在 iOS 15.4 或更高版本上测试 最后 我们来看看 订阅计费重试和宽限期 计费重试是一种尝试续订订阅时 发生错误的状态 如一张过期的信用卡 在 App Store 中 计费重试期间 App Store 将尝试修复问题 并恢复订阅 你可以选择启用宽限期 这使人们可以在 计费重试状态开始时 在有限时间内继续使用他们的订阅 让我演示一下在 Xcode 中进行测试时 如何对此进行模拟 要想在订阅续订上模拟计费问题 我们可以打开正在测试的 StoreKit Configuration 上的“编辑器”菜单 并启用 “Billing Retry on Renewal”
我希望 Food Truck 支持计费宽限期 所以我们也在菜单中 启用 “Billing Grace Period”
我们也会加快订阅速度 所以我们可以观察状态如何变化
我们先订阅 Social Feed+
现在让我们等待续订的时机
交易到期时 注意我们首先进入计费宽限期状态 我们可以看看交易检查器 并查看每个状态即将结束的时间
计费宽限期刚刚到期 现在我们处于标准计费重试状态 我们可以随时使用 “Resolve Issue For Transaction”按钮 模拟修复计费错误 让我们测试一下解决这个问题
现在问题解决了 我们得到了一个新的交易
只要我们能 启用 “Billing retry on renewal” 每笔新交易就都将继续进入计费重试 所以我们想重复这个测试 多少次都可以 妥善地处理计费重试和宽限期 是通过减少非自愿流失 来留住订阅者的关键 我们来看看用 Xcode 模拟这些状态 有多简单 现在就让我们看看 如何使用 StoreKit 处理它们 随着计费重试和宽限期状态 发生变化 状态更新序列将发出一个新值 由于我们在 Food Truck 中 提供了一个计费宽限期 我们需要确保订阅者在宽限期内 可以访问 Social Feed+ 使用 GracePeriodExpirationDate 更新信息上的属性 我们可以看到订阅者的宽限期 应该是多久 要想检查计费重试 我们只需要检查 isInBillingRetry 即可
我们也可以使用 Status 的 state 属性 轻松地检测到这些状态中的任何一个 如果我们看到一个客户 处于这些状态中的任何一个 那么我们就可以将他们引导至深层链接 到 App Store 解决计费问题 如果你正在使用 任何当前授权的API 当它们处于宽限期内时 你会收到为过期订阅准备的交易 我们也可以通过设置 billingGracePeriodIsEnabled 和我们的 StoreKit 测试课程中的 shouldEnterBillingRetryOnRenewal 来控制计费重试 和我们单元测试中的宽限期 在我们的 App 注意到 订阅进入计费重试后 测试交易的 hasPurchaseIssue 属性将为真 等各种状态更新 以及按预期确定我们的 App 更新后 我们可以使用解决交易问题的方法 来模拟 App Store 恢复订阅 计费重试和宽限期 可在 Xcode 13.3 或更高版本 或稍后在所有平台上测试 在稍后的课程中 Peter 会进一步详细介绍 如何在 iOS 和 iPadOS 16 的 沙盒上测试这些状态
我们了解了从要求退款 到处理计费重试和宽限期的 高级测试用例 关于如何使用新的 StoreKit API 支持其中一些用例的 更多详细信息 请查看 “What's new with in-app purchase.”
刚刚是对今年 Xcode 的 StoreKit 测试的新功能的 一个快速概述 但是我们并没有全面讲解 这里有新的订阅续订率 你可以测试 Xcode 中的 StoreKit 2 App 内管理项目订阅工作表 也可以使用 StoreKitTest 为你的SKAdNetwork implementations 编写单元测试 请查看 "What's new in SKAdNetwork” 以了解更多信息 现在 Peter 将带你了解今年的 沙盒测试环境中 有哪些新的内容 Peter: 谢谢 Greg 大家好 我是 Peter 我是一名 App Store 服务器工程师 我们在 Xcode 中看到了 StoreKit 测试的新功能 可以帮助你测试更复杂的 App 内购买项目实现 我们一直在倾听你的反馈 我们知道你们中的许多人依赖于 App Store 沙盒环境 来测试你的 App 内购买项目 和服务器实现 我很高兴能分享一些 我们在沙盒中所做的新的加强功能 这样你就可以更轻松地测试你的 App 和在线测试环境中的服务器 我们将推出加强功能 创建 Sandbox Apple ID App Store 连接 API 和计费失败模拟 要想使用沙盒环境 我们首先需要在 App Store Connect 中 设置一个 Sandbox Apple ID 你会注意到 我们将沙盒测试人员列表 移动到了用户和访问页面上的导航栏 在这里 我们可以用加号按钮 创建一个新的测试器 我们通过删除几个字段 从新的测试仪窗口 简化了创建过程 我们现在只问最少的信息 这样你就可以在 没有不必要信息的情况下 继续创建你的帐户 你也可以用你的电子邮件地址中 的 “plus symbol” 不需要为每个测试人员 创建全新的电子邮件地址 我们知道创建强密码可能很乏味 而我们也让这变得更容易了 我们还添加了在线建议 有助于使你的密码更安全
我们希望精简 Apple ID 创建表格 更好地进行密码复杂性提示 帮助你在设置帐户时 花费更少的时间 并有更多时间开发你的 App App Store Connect 是 你可以创建和管理 Sandbox Apple ID 并管理你的 App 内容和组织的 中心位置 过去几年里 我们一直在按你的要求 向沙盒添加功能 比如更改沙盒帐户区域 并清除购买历史记录 在 App Store Connect 中 或 Sandbox Manage Subscriptions 页面上的设备端有许多功能是可访问的 今年晚些时候 我们将把沙盒的几个功能 加进 App Store Connect API 包括查询 Sandbox Apple ID 列表 清除购买历史记录 和设置中断购买状态 利用你的沙盒帐户 可以实现更快的测试 并帮助你为常用的测试工具 设置自动化客户端 最后 我很高兴地宣布 支持沙盒中的计费失败模拟 2018 年 我们宣布了 自动续订订阅的 Billing Retry 和 Grace Period 以帮助你减少非自愿流失 自 2019 年推出以来 Billing Grace Period 允许你 恢复为你的客户提供有偿服务的 3亿天的记录 这会为你的业务带来增量收入 而你的客户在服务中不会遇到中断 当你们中的许多人已经在处理 生产中的计费失败案例时 我们也想提供更多的沙盒测试场景 这样你就可以在你的 App 先于 App Store 发布之前 测试和处理计费失败 你可以使用新的沙盒帐户设置页面 以便为你的帐户启用计费失败模拟 在你的 App 上下文中 测试订阅失败的前景和背景 并使用 verifyReceipt App Store 服务器 API 和沙盒中的 App Store 服务器通知 V2 验证订阅状态 关于计费重试 以及减少非自愿流失的更多信息 我推荐 2018 年 的 WWDC 课程 “Engineering Subscriptions” 今年 我们在沙盒帐户设置中 引入了一个开关 用于模拟失败的 App 内购买项目尝试 这也是沙盒订阅页的新主页 启用计费失败模拟后 前台 App 内购买项目将失效 该行为与你的客户的付款方式 被拒绝时的行为相匹配 计费失败模拟还确保了 自动续订订阅状态 与那些生产中的计费失败相匹配 这意味着你可以为你的那些 有帐单问题的客户 测试 App 内购买项目的消息传递 这些订阅状态会通过 V2 通知 体现在你的 App 内购买项目收据中 让我们回顾一下订阅生命周期 当你在沙盒中购买自动续订时 你已经收到了 V2 通知 比如 SUBSCRIBED 和 DID_RENEW 当你为带有活跃订阅的账户 测试失败的 App 内购买项目尝试时 下一次更新将进入计费重试状态 你现在将在沙盒中 收到结算重试的通知 比如 DID_FAIL_TO_RENEW 如果你在我们停止尝试恢复 你的订阅续订之前 禁用计费失败模拟 那么下一次续订尝试将会成功 你会收到具有子类型 BILLING_RECOVERY 的 DID_RENEW 通知 如果我们达到重试次数的限制 而且计费失败模拟已启用 那么订阅将会过期 你会收到带有 子类型 BILLING_RETRY 的 EXPIRED 如果你已经在生产中使用了宽限期 和沙盒中的 V2 通知 那么你预计可以收到 带有子类型 GRACE_PERIOD 的 DID_FAIL_TO_RENEW 通知 这是一个计费重试状态下的示例订阅 带有宽限期 如果计费失败模拟 仍然在宽限期结束时启用 那么你将收到 带有子类型 GRACE_PERIOD 的 DID_FAIL_TO_RENEW 通知 以及 GRACE_PERIOD_EXPIRED 使用 App Store 服务器 API 验证订阅信息时 你可以通过解码 signedRenewalInfo 的有效载荷 来验证订阅状态 这里 我们看到了 expirationIntent 而且计费重试字段已被填充 使用处于计费重试状态的订阅收据 调用 /verifyReceipt 时 你会看到 is_in_billing_retry_period 标志 设置了 1 此外 在使用宽限期时 你现在可以期望 填充宽限期到期日期字段 完成沙盒中的计费失败测试之后 你可以在 Sandbox Account Settings 中 禁用开关 我们希望这种新的可测试性 能帮助你为客户 打造最好的可能体验 今天我们讨论了几个 你可以使用的新测试功能 以简化测试你的 App 的 App 内购买项目功能 通过在带有 Xcode 的 App Store Connect 中同步你的配置 在本地测试或使用沙盒环境测试时 你可以使用相同的 App 内 购买项目配置 报价代码以及 Xcode 中的退款测试 等新功能 将帮助你验证复杂的 StoreKit 实现 而且订阅管理的可测试性 将使你能够发展你的 App 以确保出色的客户体验 即使他们的服务被中断 说到计费失败影响订阅收据 以及沙盒中的 App Store 服务器通知 V2 的细节 我推荐你观看 WWDC 21 的课程 “Manage in-app purchases on your server” 还有 若想了解 App Store 服务器 API 和 V2 通知的 最新消息 请查看 “What's new with in-app purchase” 我们期待听到你 关于这些新功能的反馈 感谢你的加入
-
-
6:58 - Subscription option view
VStack(alignment: .leading) { Text(subscription.displayName) .font(.headline.weight(.semibold)) Text(subscription.description) }
-
11:18 - Refund view
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() } } } }
-
12:33 - Refunds emit an updated value from the transaction updates sequence
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#> } <#...#> }
-
14:21 - Offer code view
struct SubscriptionPurchaseView: View { @State private var redeemSheetIsPresented = false var body: some View { Button("Redeem an offer") { redeemSheetIsPresented = true } .buttonStyle(.borderless) .frame(maxWidth: .infinity) .padding(.vertical) .offerCodeRedeemSheet(isPresented: $redeemSheetIsPresented) } }
-
for await verificationResult in Transaction.updates { guard case .verified(let transaction) = verificationResult else { <#Handle failed verification#> } <#Handle updated transaction#> } for await updatedStatus in Product.SubscriptionInfo.Status.updates { guard case .verified(let renewalInfo) = updatedStatus.renewalInfo else { <#Handle failed verification#> } <#Handle updated status#> }
-
16:31 - Check the active offer on the transaction value
for await status in Product.SubscriptionInfo.Status.updates { let transaction = try status.transaction.payloadValue let renewalInfo = try status.renewalInfo.payloadValue if let currentOfferType = transaction.offerType { switch currentType { case .introductory: <#Handle introductory offer#> case .promotional: <#Handle promotional offer#> case .code: <#Handle offer for codes#> default: <#Handle unknown offer type#> } self.hasCurrentOffer = true } <#...#> }
-
16:49 - Check the next pending offer on the renewal info value
for await status in Product.SubscriptionInfo.Status.updates { let transaction = try status.transaction.payloadValue let renewalInfo = try status.renewalInfo.payloadValue <#Check active current offer#> if let nextOfferType = renewalInfo.offerType { switch currentType { case .introductory: <#Handle introductory offer#> case .promotional: <#Handle promotional offer#> case .code: print("Customer has \(renewalInfo.offerID) queued") <#Handle offer for codes#> default: <#Handle unknown offer type#> } self.hasQueuedOffer = true } <#...#> }
-
18:45 - Messages updates loop
private var pendingMessages: [Message] = [] private func updatesLoop() { for await message in Message.messages { if <#Check if sensitive view is presented#>, let display: DisplayMessageAction = <#Get display message action#> { try? display(message) } else { pendingMessages.append(message) } } }
-
20:53 - Price increase changes emit an updated value from the status updates sequence
for await status in Product.SubscriptionInfo.Status.updates { let renewalInfo = try status.renewalInfo.payloadValue if renewalInfo.priceIncreaseStatus == .agreed { print("Customer consented to price increase") <#Handle consented to price increase#> } if renewalInfo.expirationReason == .didNotConsentToPriceIncrease { print("Customer did not consent to price increase") <#Handle expired due to not consenting to price increase#> } <#...#> }
-
21:19 - Unit testing price increases
let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>") session.disableDialogs = true <#Purchase a subscription#> var transaction: SKTestTransaction! = session.allTransactions().first session.requestPriceIncreaseConsentForTransaction(identifier: transaction.identifier) transaction = session.allTransactions().first XCTAssertTrue(transaction.isPendingPriceIncreaseConsent) <#Assert app updates for pending price increase#> // Write a test case for consenting and cancelling due to price increase: session.consentToPriceIncreaseForTransaction(identifier: transaction.identifier) // OR session.declinePriceIncreaseForTransaction(identifier: transaction.identifier) session.expireSubscription(productIdentifier: "<#Product ID#>") <#Assert app updates for finished price increase#>
-
24:57 - Billing retry and grace period status changes emit an updated value from the status updates sequence
for await status in Product.SubscriptionInfo.Status.updates { let renewalInfo = try status.renewalInfo.payloadValue if let gracePeriodExpirationDate = renewalInfo.gracePeriodExpirationDate, gracePeriodExpirationDate < .now { print("In grace period until \(gracePeriodExpirationDate)”) <#Allow access to subscription#> } else if renewalInfo.isInBillingRetry { <#Handle billing retry#> } <#...#> }
-
25:27 - Using the state property of a status value to check for billing retry states
struct SubscriptionStatusView: View { let currentSubscription: Product let status: Product.SubscriptionInfo.Status @Environment(\.openURL) var openURL var body: some View { Section("Your Subscription") { <#...#> if status.state == .inBillingRetryPeriod || status.state == .inGracePeriod { VStack { Text(""" There was a problem renewing your subscription. Open the App Store to update your payment information. """) Button("Open the App Store") { openURL(URL(string: "https://apps.apple.com/account/billing")!) } } } } } }
-
25:41 - Current entitlement APIs will account for grace period
for await entitlement in Transaction.currentEntitlements { <#Grant access to product#> }
-
25:50 - Unit testing billing retry and grace period
let session: SKTestSession = try SKTestSession(configurationFileNamed: "<#Configuration name#>") session.billingGracePeriodIsEnabled = true session.shouldEnterBillingRetryOnRenewal = true <#Purchase a subscription#> wait(for: [<#XCTExpectation#>], timeout: 60) let transaction: SKTestTransaction! = session.allTransactions().first XCTAssertTrue(transaction.hasPurchaseIssue) <#Assert app still allows access to subscription due to grace period#> wait(for: [<#XCTExpectation#>], timeout: 60) <#Assert app detects billing retry and no longer allows access to subscription#> session.resolveIssueForTransaction(identifier: transaction.identifier) <#Assert app allows access to subscription#>
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。