应用内购:自动更新订阅教程

来源:互联网 发布:java rpc 框架 编辑:程序博客网 时间:2024/06/09 23:43

原文:In-App Purchases: Auto-Renewable Subscriptions Tutorial
作者:Chris Wagner
译者:kmyhy

苹果在 WWDC 2016 前推出了一个新特性:可自动更新订阅。没有比什么特性比它更能让你的钱包变厚了。

通常的内购中,苹果会拿走 30% 的份额而你获得剩余的 70%。如果用户订阅了 1 年的自动更新订阅,苹果只拿走 15% 而剩余 85% 就是你的。

对于那些销售订阅式内容的开发者来说真是个利好消息。

在本教程中,你将学习如何创建自动更新订阅并将它们通过 APP Store卖给用户。

前提

在学习本教程之前,你要熟悉应用内购。如果你不熟悉,你可以阅读我们的App 内购: 不可更新订阅教程——至少应该阅读它的 Implementing Non-Renewing Subscriptions: Overview 一节。

你还需要拥有一个付费的 iTunes Connect 账号以及一台真实设备。不幸的是,模拟器和免费的苹果开发者账号是不行的。

开始

对于 RW 团队成员来说,“周五自拍”是每周的例行公事。在这一天,我们需要将最原始的自拍照发到内部的 Slack 频道上。你根本想不到,Ray 在每一周都显得是那么的衣冠楚楚无可挑剔!

到目前为止,这些照片只能在团队成员之间流传。太不应该了!这些自拍照应该让所有人看到——呃,当然不是免费的。

在本教程中,你将创建一个 App,向用户提供如下可更新订阅:

  1. 每周的 1 张自拍:$0.99/周
  2. 每周的 1 张自拍加 1 周的试用期:$2.99/月
  3. 每周的所有自拍:1.99/周
  4. 每周的所有自拍加 1 周的试用期:$5.99/周

好便宜,不是吗?

这个 App 用一个简单的 Collection View 来显示自拍照。有一条 App 商店评审指南——你应该在后面阅读它——每个订阅过的用户都会看到这些内容。

我们会用一个模拟的“服务器”根据用户订阅状态来提供自拍照。它已经包含在教程中了,你可以将注意力放到可自动更新的订阅上,而不是 App 的设计上。

这个服务器充当手机和 App Store 沙盒之间的中间层,它为自动更新订阅提供了一个沙盒环境。在实践中,这个沙盒以更高的频率模拟了订阅更新。这允许你不用等几周才能测试你的应用。

下表显示了真正的周期是如何转换成沙盒周期的:

下载开始项目,打开 RW Selfies.xcodeproj。

在项目导航器中,选择蓝色的 RW Selfies 文件夹,然后选择 Targets 下面的 RW Selfies。

将 bundle ID 修改为其它标识——比如,com.yourcompanyaddress.selfies。

在 Singing 一栏,勾选 Automatically Manage Signing,然后选择你的付费的开发团队。Xcode 会自动创建 provioning profile 和签名证书。

在真机上运行 App。记住,是真机——模拟器上能够启动应用但无法完成本教程的所有步骤。

你会看到 App 启动画面。点击 Subscribed? Come on in…。

等下!不是需要订阅才能看到自拍照的吗?

开始项目是慷慨大方的。虽然你还没有购买订阅,开始项目允许你无限制地访问第一个星期的内容。别担心会损失部分收入——你会修改设置,以防止这些无比珍贵的内容变成免费的 :]

StoreKit 简介

其它 iOS 开发领域一样,这里也有一个 kit。StoreKit 允许你和 iTunes Connecdt 交互,请求 App 内购信息。它还负责请求支付服务。

在使用 Storekit 之前,你必须在 iTunes Connect 中创建 4 个订阅套餐。

创建自动更新订阅

实现,你需要在 iTunes Connecdt 上创建一个用于这个教程的 App,用你的付费开发者账号登录 iTunes Connect。

从 dashboard 中,选择 My Apps,点击 + 按钮,从下拉菜单中选择 New App。

填写 App 信息:

  • Platforms: iOS
  • Name: RW Selfies – [Your Name] (或者其它唯一标识)
  • Primary Language: English (U.S.)
  • Bundle ID: 选择在 Xcode 中的 Bundle ID
  • SKU: 输入你所有 app 中唯一标识——如果你不知道要输入什么,请使用你的 Bundle ID

点击 Create,进入 App 的设置界面。

从导航栏左上角,选择 Features 标签页,点击 In-App Purchases (0) 旁边的 + 按钮。

选择 Auto-Renewable Subscription。

注意:如果你没有看见 Auto-Renewable Subscription 选项,你可能需要用你的账号签署某些协议。
按下 Cancel,选择左上角的 My Apps。然后从菜单中点击 Agreements,Tax and Banking。
确认同意所有的协议,尤其是付费 App 协议。
然后,会出现 “Processing” 的状态。如果这样,你可以回到 In-App Purchages 页并创建新的自动更新订阅。

为第一个内购项目输入下列信息:

  • Reference Name: All Access Weekly
  • Product ID: [你的 Bundle ID].sub.allaccess (将 [你的 Bundle ID] 换成你自己的 Xcode Bundle ID,这个名称是全局唯一的)
  • Subscription Group Reference Name: Selfies

点击 Create,你将进入 in-app configuration 页,这里需要输入如下信息:

  • Cleared for Sale: 勾选(允许上架销售)
  • Duration: 1 week
  • Free Trial: None
  • Starting Price: 1.99 USD, territories 中的值全部保持默认
  • Localizations: Add English (U.S.)
  • Subscription Display Name: All Access (Weekly)
  • Description: 能够查看 RaynWenderlich.com 团队成员每周的全部自拍照。订阅周期为 1 周。

在本教程中,你可以跳过 Review Information 步骤,但对于真实项目请不要这么做。

点击 Save,完成订阅的创建。

做完一个后,继续后面 3 个。使用下面的配置,重复创建 2、3、4 四个订阅选项。

第二个订阅选项

  • Reference Name: All Access Monthly
  • Product ID: [你的 Bundle ID].sub.allaccess.monthly
  • Subscription Group Reference Name: Selfies
  • Cleared for Sale: 勾选
  • Duration: 1 Month
  • Free Trial: 1 Week
  • Starting Price: 5.99 USD
  • Localizations: Add English (U.S.)
  • Subscription Display Name: All Access (Monthly)
  • Description: 允许查看 RayWenderlich.com 团队成员每周自拍中的所有自拍。订阅周期为 1 月,有 1 周的试用期。

第三个订阅选项

  • Reference Name: One a Week Weekly
  • Product ID: [Your Bundle ID].sub.oneaweek
  • Subscription Group Reference Name: Selfies
  • Cleared for Sale: 勾选
  • Duration: 1 Week
  • Free Trial: None
  • Starting Price: 0.99 USD
  • Localizations: Add English (U.S.)
  • Subscription Display Name: One a Week (Weekly)
  • Description: 允许查看 RayWenderlich.com 团队成员每周自拍的一张图片。订阅周期为 1 周。

第四个订阅选项

  • Reference Name: One a Week Monthly
  • Product ID: [Your Bundle ID].sub.oneaweek.monthly
  • Subscription Group Reference Name: Selfies
  • Cleared for Sale: 勾选
  • Duration: 1 Month
  • Free Trial: 1 Week
  • Starting Price: 2.99 USD
  • Localizations: Add English (U.S.)
  • Subscription Display Name: One a Week (Monthly)
  • Description: 允许查看 RayWenderlich.com 团队成员每周自拍中的一张图片。订阅周期为 1 月,有 1 周的试用期。

嘘,终于搞定!

配置订阅组

回到 “Selfies” 订阅组,添加一个 English (U.S.) 的 localization。

  • Subscription Group Display Name: Selfies
  • App Name Display Options: Use App Name (使用 App 名称)

点击 Save,拍一张好看的自拍照以示庆祝。

创建测试用户

接下来需要创建一个沙盒环境的测试用户,以模拟购买动作。

选择 My Apps,点击 Users and Roles。点击 Sandbox Testers,再点击 Testers(0)旁边的 + 按钮。

填完整个表单。输入的时候请小心——后面你将无法撤销或者修改它们。

注意:你必须为测试用户输入有效的 e-mail 地址。

很快,你会从苹果收到一封 email,要求你校验这个测试账号。你必须校验你的 email,否则无法进行任何购买。

为了简便,请保持这个账号的登录状态,在后面的教程中还要用到这个账号。

获取共享密钥

在 iTunes Connect 中还需要做的最后一件事情是获取共享密钥。

点击 My Apps,选择 RW Selfies。进入 Features,点击 In-App Purchases 表格左上角的 View Share Secret。有可能,你还需要点击 Generate Shared Secret 按钮。

不要关闭这个页面,等会你要用到它。

所有工作都是在 iTunes Connect 中进行的!幸好这个繁琐的过程已经结束了 ��

实现 StoreKit

终于可以回到示例项目编写代码了!下面,将你需要进行的步骤罗列一下:

  1. 防止不付费就能访问内容
  2. 加载产品 ID
  3. 抓取产品信息
  4. 将产品信息显示给用户
  5. 允许用户购买
  6. 处理交易
  7. 让付费内容在 App 中可见
  8. 完成交易
  9. 恢复购买交易

Step 1: 防止不付费就能访问内容

在编写购买逻辑之前,你需要先锁住内容。这些漂亮的脸蛋不是免费的!

打开 SelfiesViewController.swift 将 viewDidLoad 替换为:

override func viewDidLoad() {  super.viewDidLoad()  guard SubscriptionService.shared.currentSessionId != nil,    SubscriptionService.shared.hasReceiptData else {    showRestoreAlert()    return  }  loadSelfies()}

这段代码防止用户在没有建立会话,也没有回执数据时加载付费内容。如果缺少任何一个,都会显示一个 alert,提示用户恢复购买。

注意:苹果要求 App 支持恢复购买,因此你的 App 必须这样做。我们会在第 9 步这样做。

Steps 2 到 4: 加载、抓取和展现

在这一部分,我们将添加允许用户访问自拍照的逻辑。

2. 加载产品 Id

3. 抓取产品信息

4. 将产品显示给用户

为了简单起见,我们会硬编码产品 ID——也就是你在 iTunes Connect 设置的内容。

当你在自己的 app 中实现时,你应该通过 REST 调用或者某些类似的方式来获得这些 ID。这样你就可以根据某些特殊事件或者在“假期大促”的时候显示某些特别的购买选项。

打开 SubscriptionService.swift: 这是一个单例服务,允许你在整个 App 中通过它来调用 StoreKit。

在文件顶部加入:

import StoreKit

将 loadSubscriptionOptions 替换成如下内容并暂时忽略编译错误:

func loadSubscriptionOptions() {  let productIDPrefix = Bundle.main.bundleIdentifier! + ".sub."  let allAccess = productIDPrefix + "allaccess"  let oneAWeek  = productIDPrefix + "oneaweek"  let allAccessMonthly = productIDPrefix + "allaccess.monthly"  let oneAWeekMonthly  = productIDPrefix + "oneaweek.monthly"  let productIDs = Set([allAccess, oneAWeek, allAccessMonthly, oneAWeekMonthly])  let request = SKProductsRequest(productIdentifiers: productIDs)  request.delegate = self  request.start()}

我们用在 iTunes Connect 中定义的产品 ID 来创建 SPProductsRequest。设置 delegate 为 self,开始请求。

声明 SubscriptionService 实现 SKProductsRequestDelegate 协议:

extension SubscriptionService: SKProductsRequestDelegate {  func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {    options = response.products.map { Subscription(product: $0) }      }  func request(_ request: SKRequest, didFailWithError error: Error) {    if request is SKProductsRequest {      print("Subscription Options Failed Loading: \(error.localizedDescription)")    }  }}

一旦 SKProductsRequest 请求成功,productsRequest(_:didReceive:) 方法会被调用。它会将接受到的 SKProduct 数组转换成一个 Subscription 数组并赋给 options。注意 Subscription 是一个简单模型,以免我们在 app 中直接调用 SKProduct。

请求也可能因为某些错误而失败,这样会导致 request(_:didFailWithError:) 方法被调用。

然后,我们来调用这个 loadSubscriptionOptions 方法。打开 AppDelegate.swift 然后修改方法:

func application(_ application: UIApplication,                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {  SubscriptionService.shared.loadSubscriptionOptions()  return true}

运行 App,来看到运行效果。点击 Subscribe Now 按钮,查看订阅选项。

干得不错!我们成功地从 iTunes Connect 加载了产品信息。

注意:在 App 只保存了产品 ID 这一个订阅相关的数据。你不应该保存诸如价格、描述等内容。这些内容应该由 iTunes Connect 负责。当你想修改价格、销售地区或本地化描述的时候,你应该在 iTunes Connect 中进行。

Step 5: 用户发起购买

我们已经显示出订阅选项了,应该让用户真正去购买一次,对吗?这些自拍照可是物有所值的!

打开 SubscribeViewController.swift 找到 tableView(_:didSelectRowAt:) 方法。当用户点中表格中的某一行时,让他们进行购买。

将 TODO 注释部分替换为:

guard let option = options?[indexPath.row] else { return }SubscriptionService.shared.purchase(subscription: option)

从 options 数组中获取用户所选择的 Subscription 对象并将它传递给 SubscriptionService 的 purchase(subscription:)方法。

很简单吧?你可能想看看 purchage(subscription:) 方法中干了些什么。呃,它什么也没干!它还等你去实现呢!;]

打开 SubscriptionService.swift 修改 purchase(subscription:) 方法为:

func purchase(subscription: Subscription) {  let payment = SKPayment(product: subscription.product)  SKPaymentQueue.default().add(payment)}

它创建了一个 SKPayment,使用 subscription 对象所带的 SKProduct 作为参数,然后将这个 SKPayment 添加到默认的支付队列。然后呢?呃,后面的工作是在委托中进行的。我们必须为支付队列设置 delegate 属性,也就是下一步。

Step 6: 处理交易

打开 AppDelegate.swift 加入 import 语句:

import StoreKit

在 application(_:didFinishLaunchingWithOptions:) 方法顶部,添加下句——暂时忽略编译错误:

SKPaymentQueue.default().add(self)

这句将 AppDelegate 注册为一个 SKPaymentTransactionObserver 对象。在 AppDelegate 类定义下面,添加一个扩展,实现这个协议:

extension AppDelegate: SKPaymentTransactionObserver {  func paymentQueue(_ queue: SKPaymentQueue,                    updatedTransactions transactions: [SKPaymentTransaction]) {  }}

当某些和购买交易有关的事件发生时,paymentQueue(_:updatedTrasactions:) 方法被调用。让 AppDelegate 来观察这些事件是一种 convenience-driven 实现,因为它是单例的,在整个 App 运行期间都可用。当然,你也可以将这个逻辑迁移到一个自定义类中,但你必须确保你的 App 无论在什么时候都能处理这些事件。

将上述方法实现为如下代码——再次忽略错误警告:

for transaction in transactions {  switch transaction.transactionState {  case .purchasing:    handlePurchasingState(for: transaction, in: queue)  case .purchased:    handlePurchasedState(for: transaction, in: queue)  case .restored:    handleRestoredState(for: transaction, in: queue)  case .failed:    handleFailedState(for: transaction, in: queue)  case .deferred:    handleDeferredState(for: transaction, in: queue)  }}

这个方法会接收到一个 SKPaymentTransaction 对象数组,修改 transactionState,但不处理交易。

每个 case 分支都需要不同的处理。在这个方法后面添加:

func handlePurchasingState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {  print("User is attempting to purchase product id: \(transaction.payment.productIdentifier)")}func handlePurchasedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {  print("User purchased product id: \(transaction.payment.productIdentifier)")}func handleRestoredState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {  print("Purchase restored for product id: \(transaction.payment.productIdentifier)")}func handleFailedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {  print("Purchase failed for product id: \(transaction.payment.productIdentifier)")}func handleDeferredState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {  print("Purchase deferred for product id: \(transaction.payment.productIdentifier)")}

现在,我们只针对每种状态打印一些信息。

你可以根据每种状态来的字面意义来理解它们的含义,后面再来细究每种状态。但,deferred 是什么鸟?

Deferred 的意思是用户请求购买,但需要其它用户许可。比如,有个小孩想看看这个独一无二的自拍照 App,但需要他妈妈用家庭账号来同意这次购买。

Steps 7 到 8

在这部分,我们会让内容有效并完成交易。

通常,交易并没有在上一个步骤中结束。它只是通知支付队列的委托对象,有一个新的交易而已。交易的处理需要更多逻辑,它属于让内容对 App 可见的一部分。

苹果会通知你任何成功购买消息。此时,你应该决定什么时候以及如何分享内容。

当购买了自动更新订阅之后,针对这个 App 的用户的回执数据会根据订阅信息进行更新。回执以加密二进制数据的形式放在用户的设备中。

整个过程十分复杂,远超出本教程所允许的范畴。简单说,App 运行一个服务,将回执数据替换成 Session ID。这个 Session ID 会用于加载自拍照。

注意:这个 App 调用了 SelfieService,它接收回执并直接上传到苹果回执校验服务。这是一种欺骗手段,目的是将教程的主题集中在创建订阅上。在真实的 App 中,你应当用远程服务器而不是 App 来进行这个。
主要原因是 SelfieService 很容易受到中间人攻击。它给黑客留下了一个机会,去拦截请求并返回一个假的成功响应,这样请求无法到达苹果的检验服务。
用远程服务器处理校验和内容的分发可以避免这个问题。这样,中间人攻击就不可能了。除了安全问题,你还要考虑内容是否值得花功夫去保护。
在 In-App Purchase Video Tutorial: Part 4 Receipts 中,Sam Davies 花了大量的工作去阐释这些细节。

回到 SelfieService,有一个地方需要修改:添加你的 iTunes Connect 共享密钥。打开 SelfieService.swift 将文件顶部的 YOUR_ACCOUNT_SECRET 替换为你在 iTunes Connect 中获得的内容。

回到 AppDelegate,实现状态的处理。首先是 handlePurchasedState(for:in:):

func handlePurchasedState(for transaction: SKPaymentTransaction, in queue: SKPaymentQueue) {  print("User purchased product id: \(transaction.payment.productIdentifier)")  queue.finishTransaction(transaction)  SubscriptionService.shared.uploadReceipt { (success) in    DispatchQueue.main.async {      NotificationCenter.default.post(name: SubscriptionService.purchaseSuccessfulNotification, object: nil)    }  }}

这个方法先输出一段消息,以便你能够在控制台中看到方法被调用了,然后将交易标记为完成。这是很重要,因为这可以避免支付队列重复通知你这个交易。

调用 SelfieService 上传设备回执数据,当上传完成,发出一个通知。

打开 SubscriptionService.swift 找到 uploadReceipt(completion:) 方法。这个方法从设备读取回执数据,然后上传到 SelfieService。当执行成功,SubscriptionService 会持有回执中的 Session ID 和 当前订阅。

我们还需要实现 loadReceipt():

private func loadReceipt() -> Data? {  guard let url = Bundle.main.appStoreReceiptURL else {    return nil  }  do {    let data = try Data(contentsOf: url)    return data  } catch {    print("Error loading receipt data: \(error.localizedDescription)")    return nil  }}

回执数据放在 App 的 main bundle 中,幸好,我们可以很容易就拿到它的 URL。
如果 Bundle.main.appStoreReceiptURL 返回一个 URL,这个方法会试图加载它的数据。如果 data 有效,返回 data,否则返回 nil。

要在 App Store 沙盒环境中完成购买,你必须登出你的个人 App 商店账号——打开“设置\iTunes 和应用商店”,点击你的苹果账号,选择注销。不要试图在这里用你的沙盒测试账号进行登录。

运行 app。

点击 Subscribe Now,选择购买 One a Week(Weekly)。当对话框弹出,点击“用已有的 Apple ID”,然后输入你的沙盒测试账号。

确认购买,然后点击返回,然后点击 Subscribed? Come on in…。你会看到 Andy 问你:为什么会花钱看他的照片?

太好了!你已经成功地实现并测试了 App 内购中的自动更新订阅。记住沙盒环境调快了订阅时间,因此 3 分钟后你才能看下一张图片。

这个 App 不能自动刷新,所以你需要用返回键和 Subscribed? Come on in… 来看下一次自拍。

在等待的同时,准备将你的订阅升级到 All Access(Weekly)。

需要对沙盒环境进行一点说明,对于同一个沙盒用户,它只能自动每天自动更新 6 次订阅。在此之后,你仍然可以购买,但不会自动刷新。

SelfieService 还进行了一些微调,以便使测试更加容易。当 app 打开后,它会把当前时间保存到 UserDefaults 里。之前购买过的订阅没有用了,你可以删除 App,重新安装,然后再次购买。在真实的 App 中你不应该这样做。

Step 9: 恢复购买交易

最后,你需要允许用户恢复购买。用户任何时候都会删除 App,丢失设备和升级系统。不会用用户这样想:“RW 团队的自拍太过瘾了,我想再买一次”。因此,你必须让用户能够访问他们之前购买过的内容。

如果 SelfiesViewController 在加载时没有找到回执,示例 App 允许用户恢复购买。

打开 SubscriptionService.swift将 restorePurchases() 方法修改为:

func restorePurchases() {  SKPaymentQueue.default().restoreCompletedTransactions()}

这会告诉 StoreKit,恢复所有的先前被标记为完成的交易。

还需要修改 AppDelegate,因为它是支付队列的委托对象。打开 AppDelegate.swift 在 handleRestoredState(for:in:) 的打印语句之后添加:

queue.finishTransaction(transaction)SubscriptionService.shared.uploadReceipt { (success) in  DispatchQueue.main.async {    NotificationCenter.default.post(name: SubscriptionService.restoreSuccessfulNotification, object: nil)  }}

再次将交易标记为已处理,上传回执数据,粘贴通知以解锁内容。

嗷,好大一笔流水账。懂了吗?钱,销售额…哦,太好了…!

结束

如果你想看看最终完成的项目,请看这里。注意,要运行完成项目,你仍然需要用你自己的开发团队签名,在 iTunes Connect 进行所有操作,设置你的 iTunes Connection 共享密钥。

现在你已经知道创建自动更新订阅需要做什么了,在你在自己的项目中使用它之前,你还可以参考如下资料:

  • 阅读苹果的新订阅文档页
  • 观看最新的 [WWDC 2016 视频,尤其是这两个:

    • Introducing Expanded Subscriptions in iTunes Connect
    • Using Store Kit for In-App Purchases with Swift 3
  • 阅读苹果文档中关于购买状态的内容。
  • 阅读APP 商店评审指南。
  • 阅读 Curtis Herbert 的关于订阅的文章。

希望你喜欢这篇教程。有任何问题和建议,请在下面留言。

原创粉丝点击