2

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 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you paul, for point 3, you mean that each time the user clicks the restore button is just restoring the finished transactions, so whether the user restored premium features still needs to be checked with receipts, did I understand that right?
As you mentioned, Apple doesn't allow us to validate the receipt directly with their server, so what I should do is every time the app is opened, I call SKReceiptRefreshRequest, to refresh any pending transactions, and got the up-to-date receipt, then I check the expiration date with the current date, if expired, lock all premium features?
Yes, no matter if you are executing restoreCompletedTransactions() or SKReceiptRefreshRequest, you always need to re-read the receipt afterwards.
As long as you register the SKPaymentTransactionObserver on app start (preferable only once), the receipt should always be up to date. So there is no need to trigger SKReceiptRefreshRequest on each app start. The SKReceiptRefreshRequest is only* necessary when the user bought something within your app on a different device and wants to restore this purchase on his current device or when you test your app in the sandbox. After the receipt refresh finished, you need to re-read the receipt and configure your app correspondingly.
*Like everything else, it doesn't work 100% of the time, so sometimes SKReceiptRefreshRequest also has to be executed after a renewal should have been "detected". But either way, this should only be triggered through the user because the App Store asks for permission (opens a prompt). If this happens without the user explicitly asking for this, he could be confused/scared why he is prompted for his credentials.
1

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

6 Comments

@Christian if the answer has solved your problem, please mark it as accepted :)
Thanks for your reply, so I should get the receipt whenever the app launches, and then compare the latest receipt date with the current date? What if user changed the system date
And how can I auto-renew the subscription? I found that sometimes the receipt I get has not been refreshed
If you do not have a server, you should do the validation locally. Apple doesn't want you to call the validation endpoint from within your app "Do not call the App Store server verifyReceipt endpoint from your app" (developer.apple.com/documentation/storekit/in-app_purchase/…). You are risking getting your app rejected by implementing it this way.
Another alternative if you don't want to build your own server for subscription validation you can use a third-party service for that. You can check out our solution qonversion.io. It's free for apps with under $10k in monthly revenue. Disclosure: I'm Qonversion's co-founder.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.