
大家好! 我叫Denis,我正在开发Apphud ,该服务用于分析iOS应用程序中的自动可更新订阅。
在本文中,我将告诉您如何在iOS 12和iOS 13中配置,实施和验证自动更新订阅。此外,我将告诉您一些并非所有开发人员都考虑在内的细微之处和陷阱。
在App Store Connect上设置订阅
如果您已经具有捆绑ID和创建的应用程序,则可以跳过这些步骤。 如果您是第一次创建应用程序,请执行以下操作:
您必须在Apple Developer Portal上创建一个明确的Bundle ID(应用程序ID) 。 打开一个名为“ 证书,标识符和配置文件”的页面,转到“ 标识符”选项卡。 在2019年6月,Apple最终根据ASC(App Store Connect的缩写)更新了门户的布局。

2019年Apple Developer Portal的新设计

显式捆绑包ID通常以域样式( com.apphud.subscriptionstest
)指定。 在“ 功能”部分,您会注意到In App Purchases旁边的复选标记已被选中。 创建Bundle ID ( App ID )后,转到App Store Connect。
测试用户(沙盒用户)
要测试将来的购买,您将需要创建一个测试用户。 为此,请转到“ 用户和访问”选项卡中的“ ASC”,然后转到“ 沙箱测试器”。

用户沙箱创建表单
创建测试仪时,您可以指定任何不存在的数据,最重要的是,不要忘记电子邮件和密码!
在本文结尾处,我将讨论如何使用测试凭据来测试购买。
另一个重要步骤是在“ 协议,税收和银行业务 ”部分中设置合同和银行数据。 如果您没有付费应用程序的协议,那么您将无法测试自动续订的订阅!
之后,您可以在App Store Connect中创建一个新应用程序。 指定唯一的名称,然后选择您的捆绑包ID作为包ID 。

包裹ID是您的捆绑ID
创建应用程序后,立即转到“ 功能”选项卡。
如果您已经创建了应用程序,则可以继续从此处阅读。
创建自动续订的过程包括以下几个阶段:
1.创建一个订阅标识符并创建一个订阅组 。 订阅组是具有不同期间和价格的订阅的集合,但是在应用程序中打开相同的功能。 另外,在订阅组中,您只能激活一次免费试用期,并且只能激活一个订阅。 如果希望您的应用程序同时具有两个不同的订阅,则将需要创建两组订阅。
2.填写订阅数据:持续时间,在App Store中的显示名称(不要与名称混淆)和描述。 如果将第一个订阅添加到该组,则需要指出订阅组的显示名称。 请记住,要更频繁地保存更改,ASC可能会随时冻结并停止响应。

订阅画面
3.填写订阅价格。 有两个阶段:创建价格和特价。 以任何货币表示实际价格,然后自动为所有其他国家/地区重新计算。 介绍性优惠:您可以在此处为用户提供免费试用期或预付费折扣。 促销活动是在2019年最近出现在App Store上的:促销活动使您可以为未订阅和想要返回的用户提供特别折扣。
共享密钥生成
在包含所有已创建订阅的列表的页面上,您将看到应用程序按钮的共享密钥 。 这是在iOS应用程序中验证支票所需的特殊行。 我们将需要验证检查以确定订阅的状态。
共享密钥可以有两种类型:应用程序的唯一密钥或帐户的单个密钥。 重要提示:如果您已经在App Store中拥有该应用程序,则无论如何都不要重新创建密钥,否则用户将无法验证支票,并且您的应用程序将无法按预期运行。

在此示例中,创建了三个订阅组和3个年度订阅。
复制所有订阅的ID和共享密钥,这将在以后的代码中派上用场。
软件部分
让我们开始实践。 做一个完整的购物经理需要什么? 至少应执行以下操作:
结帐
检查订阅状态
检查更新
事务恢复(不要与更新支票混淆!)
整个购买过程可以分为两个阶段:接收产品( SKProduct
类)和初始化购买过程( SKPayment
类)。 首先,我们必须指定SKPaymentTransactionObserver
协议的委托。
IAP_PRODUCTS_DID_LOAD_NOTIFICATION
通知用于更新应用程序中的UI。
接下来,我们编写一个初始化购买的方法:
func purchaseProduct(product : SKProduct, success: @escaping SuccessBlock, failure: @escaping FailureBlock){ guard SKPaymentQueue.canMakePayments() else { return } guard SKPaymentQueue.default().transactions.last?.transactionState != .purchasing else { return } self.successBlock = success self.failureBlock = failure let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) }
SKPaymentTransactionObserver
委托看起来像这样:
extension IAPManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch (transaction.transactionState) { case .purchased: SKPaymentQueue.default().finishTransaction(transaction) notifyIsPurchased(transaction: transaction) break case .failed: SKPaymentQueue.default().finishTransaction(transaction) print("purchase error : \(transaction.error?.localizedDescription ?? "")") self.failureBlock?(transaction.error) cleanUp() break case .restored: SKPaymentQueue.default().finishTransaction(transaction) notifyIsPurchased(transaction: transaction) break case .deferred, .purchasing: break default: break } } } private func notifyIsPurchased(transaction: SKPaymentTransaction) { refreshSubscriptionsStatus(callback: { self.successBlock?() self.cleanUp() }) { (error) in
成功订阅后,将调用委托方法,在该方法中,事务具有purchased
状态。
但是,如何确定订阅的到期日期? 为此,请单独向Apple请求。
检查订阅状态
使用对Apple的verifyReceipt
POST请求验证了verifyReceipt
,我们将加密的支票作为base64编码的字符串作为参数发送,并且在响应中,我们以JSON格式收到了相同的支票。 在数组中,键latest_receipt_info
将列出每个订阅的每个期间的所有交易,包括试用期。 我们只能解析答案并获取每个产品的当前到期日期。
在WWDC 2017上,他们增加了使用verifyReceipt
请求中的exclude-old-transactions
密钥仅接收每个订阅的当前检查的verifyReceipt
。
func refreshSubscriptionsStatus(callback : @escaping SuccessBlock, failure : @escaping FailureBlock){
在该方法的开头,您可以看到存在一个检查,该检查是否存在该检查的本地副本。 本地检查可能不存在,例如,是否通过iTunes安装了该应用程序。 如果没有检查,我们将无法执行verifyReceipt
请求。 我们需要首先获取当前的本地检查,然后再次尝试对其进行验证。 使用SKReceiptRefreshRequest
类完成检查的更新:
private func refreshReceipt(){ let request = SKReceiptRefreshRequest(receiptProperties: nil) request.delegate = self request.start() } func requestDidFinish(_ request: SKRequest) {
检查更新是在refreshReceipt()
函数中实现的。 如果检查成功更新,则将requestDidFinish(_ request : SKRequest)
委托方法requestDidFinish(_ request : SKRequest)
,该方法refreshSubscriptionsStatus
调用refreshSubscriptionsStatus
方法。
购买信息的解析是如何实现的? 我们返回一个JSON对象,其中有一个嵌套的事务数组(通过latest_receipt_info
键)。 我们遍历该数组,使用expires_date
键获取到期日期,如果该日期尚未到达,则将其保存。
private func parseReceipt(_ json : Dictionary<String, Any>) {
我给出了一个简单的示例,说明如何提取订阅的当前到期日期。 没有错误处理,例如,没有检查退货(添加了取消日期 )。
要确定订阅是否处于活动状态,只需将当前日期与用户默认值(按产品密钥)中的日期进行比较即可。 如果它不存在或少于当前日期,则该订阅被视为无效。
func expirationDateFor(_ identifier : String) -> Date?{ return UserDefaults.standard.object(forKey: identifier) as? Date } let subscriptionDate = IAPManager.shared.expirationDateFor("YOUR_PRODUCT_ID") ?? Date() let isActive = subscriptionDate > Date()
事务恢复在一行SKPaymentQueue.default().restoreCompletedTransactions()
。 此函数通过func paymentQueue(**_** queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])
调用委托方法func paymentQueue(**_** queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])
恢复所有已完成的事务。
从事务恢复更新支票有什么区别?
两种方法都有助于恢复您的购买数据。 但是它们有什么区别? wwdc video有一个很棒的桌子:

从WWDC恢复购买的两种方法的差异表
在大多数情况下,您只需要使用SKReceiptRefreshRequest()
,因为我们仅对收到用于后续到期日期计算的支票感兴趣。
对于自动续订的订阅,交易本身对我们而言并不重要,因此仅使用支票更新就足够了。 但是,在某些情况下,您需要使用事务恢复方法:应用程序在购买时下载内容(Apple托管的内容),还是仍然支持iOS 7以下的版本。
购物测试(沙盒测试)
以前,要测试购买,您必须从iPhone设置的App Store登录。 这非常不方便(例如,整个Apple音乐库被删除了)。 但是,现在不需要这样做:沙箱帐户现在与主帐户分开存在。

与在App Store中进行实际购买相比,购买过程相似,但是有一些要点:
您将始终需要通过系统窗口输入登录密码。 仍不支持使用Touch ID / Face ID进行购买。
如果在正确输入登录名和密码后,系统一次又一次要求输入登录密码, 请单击“取消” ,最小化应用程序,然后重试。 看起来像胡话,但它适用于许多人。 但是有时在第二次输入密码后,该过程仍然继续。
您将无法以任何方式测试退订过程。
订阅期的持续时间远少于实际时间。 并且每天更新不超过6次。
iOS 13中的StoreKit有什么新功能?
在新版本中-仅SKStorefront
类,它提供有关用户在App Store中注册的国家/地区的信息。 这对于在不同国家/地区使用不同订阅的开发人员可能有用。 以前,每个人都通过地理位置或设备区域进行检查,但这并未给出准确的结果。 现在,在App Store中查找国家/地区非常容易: SKPaymentQueue.default().storefront?.countryCode
。 如果在购买过程中App Store中的国家/地区发生变化,则还会添加方法委托。 在这种情况下,您可以自己继续或取消购买过程。
使用订阅时的陷阱
- Apple不建议直接从设备检查支票。 他们在WWDC (从5:50开始)多次谈论了这一点,并且在文档中对此进行了说明 。 这是不安全的,因为攻击者可以使用中间人攻击来拦截数据。 检查检查的正确方法是使用服务器进行本地验证。
- 检查到期日期存在问题。 如果您不使用服务器,则可以将设备上的系统时间更改为较旧的时间,然后我们的代码将给出错误的结果-订阅将被视为活动状态。 如果这不适合您,那么您可以使用发布准确世界时间的任何服务。
- 并非所有用户都可以免费试用。 用户可以在一段时间后重新安装该应用程序,并且该应用程序将显示试用版照常可用。 更新检查,验证并在JSON中检查该用户是否有试用版是正确的。 许多人没有。
- 如果用户要求退款,则
cancellation_date
将被添加到订阅JSON中,但expires_date
将保持不变。 因此,重要的是始终检查是否有expires_date
字段的存在,这是优于expires_date
更好的选择。 - 每次启动应用程序时都不值得更新支票,因为,首先,这毫无意义,其次,用户很可能会看到Apple ID密码输入窗口。 值得更新支票,例如,当用户本人单击“恢复购物”按钮时。
- 如何确定在什么时候值得验证支票以获取订阅的当前到期日期? 您可以在每次开始时或仅在订阅结束时验证支票。 但是,如果您仅在订阅结束时检查支票,则已发出退款的用户将可以免费使用您的应用程序,直到该期限结束为止。
结论
希望本文对您有所帮助。 我不仅尝试添加代码,还试图解释开发过程中的细微之处。 完整的类代码可在此处下载。 对于初学者和想要了解更多有关一切工作原理的开发人员的知识,本课程将非常有用。 对于实时应用程序,建议使用更严格的解决方案,例如SwiftyStoreKit 。
是否想在10分钟内在iOS应用中实现订阅? 整合Apphud并:
- 仅使用一种方法进行购买;
- 自动跟踪每个用户的订阅状态;
- 轻松整合订阅优惠
- 将订阅事件发送到Amplitude,Mixpanel,Slack和Telegram,并考虑到用户的本地货币;
- 降低应用程序的客户流失率并返回未订阅的用户。