简体   繁体   English

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

[英]Swift How to handle Auto-renewable Subscription receipt and validation

I am testing the auto renewable In-app purchases in swift, I found out that there are some strange problems with my code.我正在测试 swift 中的自动更新应用内购买,我发现我的代码存在一些奇怪的问题。

I am testing these functions in sandbox environment我正在沙盒环境中测试这些功能

  1. User can purchase either one month, one year auto renewable subscription or permanent permission用户可以购买一个月、一年的自动续订订阅或永久许可
  2. App should check if the subscription is still valid every time when user open app, if not, lock all premium functions每次用户打开应用程序时,应用程序都应该检查订阅是否仍然有效,如果没有,则锁定所有高级功能
  3. User is able to restore the purchased plan, app should get the previous purchased type ie.用户能够恢复购买的计划,应用程序应该获得以前购买的类型,即。 one month, one year, or permanent.一个月、一年或永久。

After long research on the tutorials, I am still confused about the validation经过对教程的长期研究,我仍然对验证感到困惑

  1. I see that there are two ways to validate receipt, one is locally the other is on the server.我看到有两种验证收据的方法,一种是在本地,另一种是在服务器上。 But I don't have a server, does that mean I can only validate it locally但是我没有服务器,是不是只能在本地验证
  2. Every time the auto-renewal subscription expires, the local receipt is not updated, so when I reopen the app I got a subscription expiration alert (The method I defined by my self for validation check ), when I click the restore button, the app restored successfully and receipt was updated每次自动续费订阅到期时,本地收据都不会更新,所以当我重新打开应用程序时,我收到了订阅过期警报(我自己定义的验证检查方法),当我点击恢复按钮时,应用程序成功恢复并更新收据
  3. After 6 times manually restored and refresh the receipt (the sandbox user can only renew 6 times), when I click the restore button, the part transaction ==.purchased is till called, and my app unlocks premium function, however when I reopen my app, my app alerts that the subscription is expired, which is it should.经过6次手动恢复并刷新收据(沙盒用户只能更新6次),当我点击恢复按钮时,部分交易==.purchased直到被调用,我的应用程序解锁高级function,但是当我重新打开我的应用程序,我的应用程序会提醒订阅已过期,这是应该的。

My core problem is how can I check the validation of subscriptions with Apple every time when I open the app, I don't have a server, and I don't know why the receipt is not refreshing automatically我的核心问题是每次打开应用程序时如何检查Apple的订阅验证,我没有服务器,我不知道为什么收据没有自动刷新

Here are some parts of my code, I call checkUserSubsriptionStatus() when I open the app, I am using TPInAppReceipt Library这是我的代码的一些部分,我在打开应用程序时调用 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()
        }
    }
    

}

If you do not have the possibility to use a server, you need to validate locally.如果您无法使用服务器,则需要在本地进行验证。 Since you are already included TPInAppReceipt library, this is relatively easy.由于您已经包含了 TPInAppReceipt 库,因此这相对容易。

To check if the user has an active premium product and what type it has, you can use the following code:要检查用户是否有活跃的高级产品以及它的类型,您可以使用以下代码:

// 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

One point I notice in your code, which could lead to problems, is that you constantly add a new SKPaymentTransactionObserver .我在您的代码中注意到可能导致问题的一点是您不断添加新的SKPaymentTransactionObserver You should have one class conforming to SKPaymentTransactionObserver and add this only once on app start and not on every public call.您应该有一个符合 SKPaymentTransactionObserver 的SKPaymentTransactionObserver并且仅在应用启动时添加一次,而不是在每次公开调用时添加。 Also, you need to remove it when you no longer need it (if you created it only once, you would do it in the deinit of your class, conforming to the observer protocol.此外,当您不再需要它时,您需要将其删除(如果您只创建一次,您将在deinit的 deinit 中执行它,符合观察者协议。

I assume this is the reason for point 2.我认为这是第 2 点的原因。

Technically, the behavior described in point 3 is correct because the method you are using asks the payment queue to restore all previously completed purchases (see here ).从技术上讲,第 3 点中描述的行为是正确的,因为您使用的方法要求支付队列恢复所有以前完成的购买(请参阅此处)。

Apple states restoreCompletedTransactions() should only be used for the following scenarios (see here ): Apple 声明restoreCompletedTransactions()只能用于以下场景(请参阅此处):

  • If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content.如果您使用 Apple 托管的内容,恢复已完成的事务会为您的应用程序提供用于下载内容的事务对象。
  • If you need to support versions of iOS earlier than iOS 7, where the app receipt isn't available, restore completed transactions instead.如果您需要支持早于 iOS 7 的 iOS 版本(应用收据不可用),请改为恢复已完成的交易。
  • If your app uses non-renewing subscriptions, your app is responsible for the restoration process.如果您的应用使用非续订订阅,则您的应用负责恢复过程。

For your case, it is recommended to use a SKReceiptRefreshRequest , which requests to update the current receipt.对于您的情况,建议使用SKReceiptRefreshRequest请求更新当前收据。

Get the receipt every time when the app launches by calling the method in AppDelegate.每次应用启动时通过调用 AppDelegate 中的方法获取收据。

getAppReceipt(forTransaction: nil) getAppReceipt(forTransaction: 无)

Now, below is the required method:现在,以下是所需的方法:

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.
            }
 }

Here is the method receiptValidation:这是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)
}

Next is the final method that verifies receipt and gets the expiry date of subscription:接下来是验证收据并获取订阅到期日期的最终方法:

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)")
                    }
                  }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM