In-App Purchase receipt not working properly

I created an In-App Purchase subscription feature in my app, but the receipt validation is not working properly. I would like to change a boolean based on the status of the purchase (e.g. subscription is active or subscription is expired). If the subscription is active I'd like to change bool to true and enable user button interaction, if expired, then change bool to false and deny button interaction.


View did load code to set the bool:


do {
            try validateReceipt()
            // The receipt is valid 
            print("Receipt is valid")
            UserDefaults.standard.set(true, forKey: "isSubbed")
        } catch ReceiptValidationError.receiptNotFound {
            // There is no receipt on the device 
            UserDefaults.standard.set(false, forKey: "isSubbed")
        } catch ReceiptValidationError.jsonResponseIsNotValid(let description) {
            // unable to parse the json   
            print(description)
        } catch ReceiptValidationError.notBought {
            // the subscription hasn't being purchased 
            UserDefaults.standard.set(false, forKey: "isSubbed")
        } catch ReceiptValidationError.expired {
            // the subscription is expired 
            UserDefaults.standard.set(false, forKey: "isSubbed")
        } catch {
            print("Unexpected error: \(error).")
        }


Here's some code for receipt validation:


enum ReceiptValidationError: Error {
    case receiptNotFound
    case jsonResponseIsNotValid(description: String)
    case notBought
    case expired
}

func validateReceipt() throws {
    guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) else {
        throw ReceiptValidationError.receiptNotFound
    }
    
    let receiptData = try! Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
    let receiptString = receiptData.base64EncodedString()
    let jsonObjectBody = ["receipt-data" : receiptString, "1111" : String()]
    
    #if DEBUG
    let url = URL(string: "https://sandbox.itunes.apple.com/verifyReceipt")!
    #else
//    let url = URL(string: "https://buy.itunes.apple.com/verifyReceipt")!
    #endif
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.httpBody = try! JSONSerialization.data(withJSONObject: jsonObjectBody, options: .prettyPrinted)
    
    let semaphore = DispatchSemaphore(value: 0)
    
    var validationError : ReceiptValidationError?
    
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, let httpResponse = response as? HTTPURLResponse, error == nil, httpResponse.statusCode == 200 else {
            validationError = ReceiptValidationError.jsonResponseIsNotValid(description: error?.localizedDescription ?? "")
            semaphore.signal()
            return
        }
        guard let jsonResponse = (try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)) as? [AnyHashable: Any] else {
            validationError = ReceiptValidationError.jsonResponseIsNotValid(description: "Unable to parse json")
            semaphore.signal()
            return
        }
        guard let expirationDate = self.expirationDate(jsonResponse: jsonResponse, forProductId: "com.test.UnlockTools.Subscription1") else {
            validationError = ReceiptValidationError.notBought
            semaphore.signal()
            return
        }
        
        let currentDate = Date()
        if currentDate > expirationDate {
            validationError = ReceiptValidationError.expired
        }
        
        semaphore.signal()
    }
    task.resume()
    
    semaphore.wait()
    
    if let validationError = validationError {
        throw validationError
    }
}
    
    func expirationDate(jsonResponse: [AnyHashable: Any], forProductId productId :String) -> Date? {
        guard let receiptInfo = (jsonResponse["latest_receipt_info"] as? [[AnyHashable: Any]]) else {
            return nil
        }
        
        let filteredReceipts = receiptInfo.filter{ return ($0["product_id"] as? String) == productId }
        
        guard let lastReceipt = filteredReceipts.last else {
            return nil
        }
        
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
        
        if let expiresString = lastReceipt["expires_date"] as? String {
            return formatter.date(from: expiresString)
        }
        
        return nil
    }

}

Currently the bool does not seem to change. Not sure the try and catch is working properly. I'll provide more code if needed. Any help would be greatly appreciated.

Replies

TMI.

I assume you are sending teh receipt to Apple servers directly from the app and getting back something. If that is correct, what are you getting back? What do you do with that?

I completely changed my receipt validation code, and now use the SwiftyStoreKit Library for validation. Here's a small example of the code for the new validation method:


func verifyPurchase(with id: String, sharedSecret: String, type: PurchaseType) {
        let appleValidator = AppleReceiptValidator(service: .production, sharedSecret: sharedSecret)
        SwiftyStoreKit.verifyReceipt(using: appleValidator) { result in
            switch result {
            case .success(let receipt):
                // Verify the purchase of a Subscription
                
                switch type {
                case .autoRenewable:
                    let purchaseResult = SwiftyStoreKit.verifySubscription(
                        ofType: .autoRenewable,
                        productId: id,
                        inReceipt: receipt)
                    
                    switch purchaseResult {
                    case .purchased(let expiryDate):
                        print("\(id) is valid until \(expiryDate)")
                    case .expired(let expiryDate):
                        print("\(id) is expired since \(expiryDate)")
                    case .notPurchased:
                        print("The user has never purchased \(id)")
                    }
                }
                
            case .error(let error):
                print("Receipt verification failed: \(error)")
            }
        }
    }

This works.

Verify the receipt from the device directly to Apple servers it is not recomended.

I would prefer to use our servers for that step in the near future. Although I was wondering what problems can occur if I stay with the current process.

The problem with sending the receipt from the device to the Apple servers directly is that you are subject to a 'man-in-the-middle' hack in which the user diverts your request to a different server that responds with a valid, current receipt.


But....what is your problem? What are you getting back from the apple servers and why can't you figure out from that information whether the subscription is valid?

Ah ok, I see. I'll definetly try to carry the process over to my server then, thanks.


My problem is actually solved using the SwiftyStoreKit. My whole subscription method works perfectly at the moment. I am just unable to mark any response as a correct answer.