Swift 如何处理自动续订订阅接收和验证

问题描述

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

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

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

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

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

我的核心问题是我每次打开app的时候怎么查看苹果订阅的验证,我没有服务器,不知道为什么收据没有自动刷新

这是我的代码的一些部分,我在打开应用程序时调用 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()
        }
    }
    

}

解决方法

如果您无法使用服务器,则需要在本地进行验证。由于您已经包含 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,它请求更新当前收据。

,

通过调用 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)")
                    }
                  }
}