原文: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 张自拍:$0.99/周
- 每周的 1 张自拍加 1 周的试用期:$2.99/月
- 每周的所有自拍:1.99/周
- 每周的所有自拍加 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
终于可以回到示例项目编写代码了!下面,将你需要进行的步骤罗列一下:
- 防止不付费就能访问内容
- 加载产品 ID
- 抓取产品信息
- 将产品信息显示给用户
- 允许用户购买
- 处理交易
- 让付费内容在 App 中可见
- 完成交易
- 恢复购买交易
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 视频,尤其是这两个:
- 阅读苹果文档中关于购买状态的内容。
- 阅读APP 商店评审指南。
- 阅读 Curtis Herbert 的关于订阅的文章。
希望你喜欢这篇教程。有任何问题和建议,请在下面留言。