【问题标题】:Swift How to handle Auto-renewable Subscription receipt and validationSwift 如何处理自动更新订阅的接收和验证
【发布时间】:2021-07-26 13:32:20
【问题描述】:

我正在快速测试自动更新的应用内购买,我发现我的代码存在一些奇怪的问题。

我正在沙盒环境中测试这些功能

  1. 用户可以购买一个月、一年的自动续订或永久许可
  2. 每次用户打开应用时,应用都要检查订阅是否仍然有效,如果没有,则锁定所有高级功能
  3. 用户能够恢复购买的计划,应用程序应该得到以前购买的类型,即。一个月、一年或永久。

经过长期研究教程,我仍然对验证感到困惑

  1. 我看到有两种验证收据的方法,一种是在本地,另一种是在服务器上。 但是我没有服务器,是不是只能在本地验证呢
  2. 每次自动续费订阅到期时,本地收据都不会更新,所以当我重新打开应用程序时,我收到订阅到期警报(我自己定义的验证检查方法),当我点击恢复按钮时,应用恢复成功,收据已更新
  3. 经过6次手动恢复并刷新收据(沙盒用户只能更新6次),当我点击恢复按钮时,部分交易== .purchased被调用,我的应用程序解锁了高级功能,但是当我重新打开我的应用,我的应用提醒订阅已过期,这是应该的。

我的核心问题是每次打开应用,我没有服务器,不知道为什么收据没有自动刷新时,如何检查Apple的订阅验证

这是我的代码的一些部分,我在打开应用程序时调用了 checkUserSubsriptionStatus(),我正在使用 TPInAppReceipt 库

class InAppPurchaseManager {
    static var shared = InAppPurchaseManager()

    
    init() {
    }
    

    public func getUserPurchaseType() -> PurchaseType {
        if let receipt = try? InAppReceipt.localReceipt() {
            var purchaseType: PurchaseType = .none
            
            if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneMonth.productID) {
                purchaseType = .oneMonth
            }
            if let purchase = receipt.lastAutoRenewableSubscriptionPurchase(ofProductIdentifier: PurchaseType.oneYear.productID) {
                purchaseType = .oneYear
            }
            
            if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
                purchaseType = .permanent
            }
            
            return purchaseType

        } else {
            print("Receipt not found")
            return .none
        }
    }
    
    public func restorePurchase(in viewController: SKPaymentTransactionObserver) {
        SKPaymentQueue.default().add(viewController)
        if SKPaymentQueue.canMakePayments() {
            SKPaymentQueue.default().restoreCompletedTransactions()
        } else {
            self.userIsNotAbleToPurchase()
        }
    }
    
    public func checkUserSubsriptionStatus() {
        DispatchQueue.main.async {
            if let receipt = try? InAppReceipt.localReceipt() {
                self.checkUserPermanentSubsriptionStatus(with: receipt)
               
               
                
            }
        }
        
    }
    

    private func checkUserPermanentSubsriptionStatus(with receipt: InAppReceipt) {
        if let receipt = try? InAppReceipt.localReceipt() { //Check permsnent subscription
            
            if receipt.containsPurchase(ofProductIdentifier: PurchaseType.permanent.productID) {
                print("User has permament permission")
                if !AppEngine.shared.currentUser.isVip {
                    self.updateAfterAppPurchased(withType: .permanent)
                }
            } else {
                self.checkUserAutoRenewableSubsrption(with: receipt)
                
            }
            
        }
    }
    
    private func checkUserAutoRenewableSubsrption(with receipt: InAppReceipt) {
        if receipt.hasActiveAutoRenewablePurchases {
            print("Subsription still valid")
            if !AppEngine.shared.currentUser.isVip {
                let purchaseType = InAppPurchaseManager.shared.getUserPurchaseType()
                updateAfterAppPurchased(withType: purchaseType)
            }
        } else {
            print("Subsription expired")
            
            if AppEngine.shared.currentUser.isVip {
                self.subsrptionCheckFailed()
            }
        }
    }
    
  
    
    
    private func updateAfterAppPurchased(withType purchaseType: PurchaseType) {
        AppEngine.shared.currentUser.purchasedType = purchaseType
        AppEngine.shared.currentUser.energy += 5
        AppEngine.shared.userSetting.hasViewedEnergyUpdate = false
        AppEngine.shared.saveUser()
        AppEngine.shared.notifyAllUIObservers()
    }
    
    public func updateAfterEnergyPurchased() {
        AppEngine.shared.currentUser.energy += 3
        AppEngine.shared.saveUser()
        AppEngine.shared.notifyAllUIObservers()
    }
    
    public func purchaseApp(with purchaseType: PurchaseType, in viewController: SKPaymentTransactionObserver) {
        SKPaymentQueue.default().add(viewController)
        
        if SKPaymentQueue.canMakePayments() {
            let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = purchaseType.productID
            SKPaymentQueue.default().add(paymentRequest)
        } else {
            self.userIsNotAbleToPurchase()
        }
    }
    
    public func purchaseEnergy(in viewController: SKPaymentTransactionObserver) {
        SKPaymentQueue.default().add(viewController)
        let productID = "com.crazycat.Reborn.threePointOfEnergy"
        if SKPaymentQueue.canMakePayments() {
            let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = productID
            SKPaymentQueue.default().add(paymentRequest)
        } else {
            self.userIsNotAbleToPurchase()
        }
    }
    

}

【问题讨论】:

    标签: swift in-app-purchase auto-renewable


    【解决方案1】:

    如果您无法使用服务器,则需要在本地进行验证。由于您已经包含了 TPInAppReceipt 库,因此这相对容易。

    要检查用户是否有活跃的高级产品以及它的类型,您可以使用以下代码:

    // Get all active purchases which are convertible to `PurchaseType`.
    let premiumPurchases = receipt.activeAutoRenewableSubscriptionPurchases.filter({ PurchaseType(rawValue: $0.productIdentifier) != nil })
    
    // It depends on how your premium access works, but if it doesn't matter what kind of premium the user has, it is enough to take one of the available active premium products.
    // Note: with the possibility to share subscriptions via family sharing, the receipt can contain multiple active subscriptions.
    guard let product = premiumPurchases.first else {
      // User has no active premium product => lock all premium features
      return
    }
    
    // To be safe you can use a "guard" or a "if let", but since we filtered for products conforming to PurchaseType, this shouldn't fail
    let purchaseType = PurchaseType(rawValue: product.productIdentifier)!
    
    // => Setup app corresponding to active premium product type
    

    我在您的代码中注意到可能导致问题的一点是您不断添加新的SKPaymentTransactionObserver。您应该有一个符合SKPaymentTransactionObserver 的类,并且只在应用程序启动时添加一次,而不是在每次公开调用时添加。此外,当您不再需要它时,您需要将其移除(如果您只创建一次,您将在您的班级的deinit 中执行此操作,符合观察者协议。

    我认为这是第 2 点的原因。

    从技术上讲,第 3 点中描述的行为是正确的,因为您使用的方法要求支付队列恢复所有之前完成的购买(请参阅here)。

    Apple 声明 restoreCompletedTransactions() 只能用于以下场景(请参阅 here):

    • 如果您使用 Apple 托管的内容,恢复已完成的交易会为您的应用提供用于下载内容的交易对象。
    • 如果您需要支持 iOS 7 之前的 iOS 版本,而应用收据不可用,请改为恢复已完成的交易。
    • 如果您的应用使用非续订订阅,则您的应用负责恢复过程。

    对于您的情况,建议使用SKReceiptRefreshRequest,它要求更新当前收据。

    【讨论】:

    • 谢谢paul,对于第3点,您的意思是用户每次点击恢复按钮只是恢复完成的交易,所以用户是否恢复了高级功能仍然需要检查收据,没有我明白了吧?
    • 正如你提到的,Apple 不允许我们直接与他们的服务器验证收据,所以我应该做的是每次打开应用程序时,我调用 SKReceiptRefreshRequest,以刷新任何待处理的交易,并得到最新的收据,然后我用当前日期检查过期日期,如果过期,锁定所有高级功能?
    • 是的,不管你是执行restoreCompletedTransactions()还是SKReceiptRefreshRequest,之后总是需要重新读取回执。
    • 只要您在应用启动时注册SKPaymentTransactionObserver(最好只注册一次),收据应该始终是最新的。因此无需在每次应用启动时触发SKReceiptRefreshRequestSKReceiptRefreshRequest 仅当用户在其他设备上购买了您的应用程序中的某些内容并希望在其当前设备上恢复此购买或您在沙盒中测试您的应用程序时才需要*。收据刷新完成后,您需要重新读取收据并相应配置您的应用程序。
    • *像其他所有东西一样,它不能 100% 工作,所以有时SKReceiptRefreshRequest 也必须在应该“检测到”续订之后执行。但无论哪种方式,这都应该只通过用户触发,因为 App Store 要求许可(打开提示)。如果在没有用户明确要求的情况下发生这种情况,他可能会感到困惑/害怕为什么会提示他输入凭据。
    【解决方案2】:

    每次应用启动时调用AppDelegate中的方法获取收据。

    getAppReceipt(forTransaction: nil)

    现在,下面是所需的方法:

    func getAppReceipt(forTransaction transaction: SKPaymentTransaction?) {
                guard let receiptURL = receiptURL else {  /* receiptURL is nil, it would be very weird to end up here */  return }
                do {
                    let receipt = try Data(contentsOf: receiptURL)
                    receiptValidation(receiptData: receipt, transaction: transaction)
                } catch {
                    // there is no app receipt, don't panic, ask apple to refresh it
                    let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
                    appReceiptRefreshRequest.delegate = self
                    appReceiptRefreshRequest.start()
                    // If all goes well control will land in the requestDidFinish() delegate method.
                    // If something bad happens control will land in didFailWithError.
                }
     }
    

    这里是receiptValidation方法:

        func receiptValidation(receiptData: Data?, transaction: SKPaymentTransaction?) {
                                
            guard let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) else { return }
            verify_in_app_receipt(with_receipt_string: receiptString, transaction: transaction)
    }
    

    接下来是验证收据并获取订阅到期日期的最终方法:

    func verify_in_app_receipt(with_receipt_string receiptString: String, transaction: SKPaymentTransaction?) {
                        
                        let params: [String: Any] = ["receipt-data": receiptString,
                                                     "password": "USE YOUR PASSWORD GENERATED FROM ITUNES",
                                                     "exclude-old-transactions": true]
                        
                        // Below are the url's used for in app receipt validation
                        //appIsInDevelopment ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
                        
                        super.startService(apiType: .verify_in_app_receipt, parameters: params, files: [], modelType: SubscriptionReceipt.self) { (result) in
                            switch result {
                                case .Success(let receipt):
                                if let receipt = receipt {
                                    print("Receipt is: \(receipt)")
                                    if let _ = receipt.latest_receipt, let receiptArr = receipt.latest_receipt_info {
                                        var expiryDate: Date? = nil
                                        for latestReceipt in receiptArr {
                                            if let dateInMilliseconds = latestReceipt.expires_date_ms, let product_id = latestReceipt.product_id {
                                                let date = Date(timeIntervalSince1970: dateInMilliseconds / 1000)
                                                if date >= Date() {
                                                    // Premium is valid
                                                }
                                            }
                                        }
                                        if expiryDate == nil {
                                            // Premium is not purchased or is expired
                                        }
                                    }
                             }
                                                        
                            case .Error(let message):
                                print("Error in api is: \(message)")
                        }
                      }
    }
    

    【讨论】:

    • @Christian 如果答案解决了您的问题,请将其标记为已接受 :)
    • 感谢您的回复,所以我应该在应用程序启动时获取收据,然后将最新的收据日期与当前日期进行比较?如果用户更改了系统日期会怎样
    • 如何自动续订?我发现有时候我拿到的收据没有刷新
    • 如果您没有服务器,您应该在本地进行验证。 Apple 不希望您从您的应用程序中调用验证端点“不要从您的应用程序调用 App Store 服务器 verifyReceipt 端点”(developer.apple.com/documentation/storekit/in-app_purchase/…)。以这种方式实施,您的应用可能会被拒绝。
    • 如果您不想为订阅验证构建自己的服务器,则可以使用第三方服务来实现此目的。您可以查看我们的解决方案qonversion.io。对于月收入低于 1 万美元的应用程序,它是免费的。披露:我是 Qonversion 的联合创始人。
    猜你喜欢
    • 2021-02-06
    • 2014-11-10
    • 2015-09-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-21
    • 2018-06-02
    相关资源
    最近更新 更多