Swift How to handle Auto-renewable Subscription receipt and validation

4.5k Views Asked by At

I am testing the auto renewable In-app purchases in swift, I found out that there are some strange problems with my code.

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.

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

Here are some parts of my code, I call checkUserSubsriptionStatus() when I open the app, I am using TPInAppReceipt Library

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

}

2

There are 2 best solutions below

5
On BEST ANSWER

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.

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. You should have one class conforming to SKPaymentTransactionObserver and add this only once on app start and not on every public call. 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.

I assume this is the reason for point 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).

Apple states restoreCompletedTransactions() should only be used for the following scenarios (see here):

  • If you use Apple-hosted content, restoring completed transactions gives your app the transaction objects it uses to download the content.
  • If you need to support versions of iOS earlier than iOS 7, where the app receipt isn’t available, restore completed transactions instead.
  • 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.

6
On

Get the receipt every time when the app launches by calling the method in AppDelegate.

getAppReceipt(forTransaction: nil)

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:

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