Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing Receipt Validation in Swift 3

I am developing an iOS app in Swift 3 and trying to implement receipt validation following this tutorial: http://savvyapps.com/blog/how-setup-test-auto-renewable-subscription-ios-app. However, the tutorial seems to have been written using an earlier version of Swift, so I had to make several changes. Here is my receiptValidation() function:

func receiptValidation() {
    let receiptPath = Bundle.main.appStoreReceiptURL?.path
    if FileManager.default.fileExists(atPath: receiptPath!){
        var receiptData:NSData?
        do{
            receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
        }
        catch{
            print("ERROR: " + error.localizedDescription)
        }
        let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
        let postString = "receipt-data=" + receiptString! + "&password=" + SUBSCRIPTION_SECRET
        let storeURL = NSURL(string:"https://sandbox.itunes.apple.com/verifyReceipt")!
        let storeRequest = NSMutableURLRequest(url: storeURL as URL)
        storeRequest.httpMethod = "POST"
        storeRequest.httpBody = postString.data(using: .utf8)
        let session = URLSession(configuration:URLSessionConfiguration.default)
        let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error in
            do{
                let jsonResponse:NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
                let expirationDate:NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse)!
                self.updateIAPExpirationDate(date: expirationDate)
            }
            catch{
                print("ERROR: " + error.localizedDescription)
            }
        }
        task.resume()
    }
}

The problem shows up when I try to call the expirationDateFromResponse() method. It turns out that the jsonResponse that gets passed to this method only contains: status = 21002;. I looked this up and it means "The data in the receipt-data property was malformed or missing." However, the device I'm testing on has an active sandbox subscription for the product, and the subscription seems to work correctly aside from this issue. Is there something else I still need to do to make sure the receiptData value will be read and encoded correctly, or some other issue that might be causing this problem?

EDIT:

I tried an alternate way of setting storeRequest.httpBody:

func receiptValidation() {
    let receiptPath = Bundle.main.appStoreReceiptURL?.path
    if FileManager.default.fileExists(atPath: receiptPath!){
        var receiptData:NSData?
        do{
            receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
        }
        catch{
            print("ERROR: " + error.localizedDescription)
        }
        let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) //.URLEncoded
        let dict = ["receipt-data":receiptString, "password":SUBSCRIPTION_SECRET] as [String : Any]
        var jsonData:Data?
        do{
            jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)
        }
        catch{
            print("ERROR: " + error.localizedDescription)
        }
        let storeURL = NSURL(string:"https://sandbox.itunes.apple.com/verifyReceipt")!
        let storeRequest = NSMutableURLRequest(url: storeURL as URL)
        storeRequest.httpMethod = "POST"
        storeRequest.httpBody = jsonData!
        let session = URLSession(configuration:URLSessionConfiguration.default)
        let task = session.dataTask(with: storeRequest as URLRequest) { data, response, error in
            do{
                let jsonResponse:NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
                let expirationDate:NSDate = self.expirationDateFromResponse(jsonResponse: jsonResponse)!
                self.updateIAPExpirationDate(date: expirationDate)
            }
            catch{
                print("ERROR: " + error.localizedDescription)
            }
        }
        task.resume()
    }
}

However, when I run the app with this code, it hangs upon reaching the line jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted). It doesn't even make it to the catch block, it just stops doing anything. From what I've seen online, other people seem to have trouble using JSONSerialization.data to set the request httpBody in Swift 3.

like image 345
user3726962 Avatar asked Sep 26 '16 19:09

user3726962


People also ask

How do I validate my Apple receipt?

Use the production URL https://buy.itunes.apple.com/verifyReceipt when your app is live in the App Store. For more information on these endpoints, see verifyReceipt. Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code.

What is receipt verification?

Receipt verification is a systematic process by which a site verifies that what was specified and ordered is actually what is received.


2 Answers

Its working correctly with Swift 4

func receiptValidation() {
    let SUBSCRIPTION_SECRET = "yourpasswordift"
    let receiptPath = Bundle.main.appStoreReceiptURL?.path
    if FileManager.default.fileExists(atPath: receiptPath!){
        var receiptData:NSData?
        do{
            receiptData = try NSData(contentsOf: Bundle.main.appStoreReceiptURL!, options: NSData.ReadingOptions.alwaysMapped)
        }
        catch{
            print("ERROR: " + error.localizedDescription)
        }
        //let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
        let base64encodedReceipt = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn)

        print(base64encodedReceipt!)


        let requestDictionary = ["receipt-data":base64encodedReceipt!,"password":SUBSCRIPTION_SECRET]

        guard JSONSerialization.isValidJSONObject(requestDictionary) else {  print("requestDictionary is not valid JSON");  return }
        do {
            let requestData = try JSONSerialization.data(withJSONObject: requestDictionary)
            let validationURLString = "https://sandbox.itunes.apple.com/verifyReceipt"  // this works but as noted above it's best to use your own trusted server
            guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return }
            let session = URLSession(configuration: URLSessionConfiguration.default)
            var request = URLRequest(url: validationURL)
            request.httpMethod = "POST"
            request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
            let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in
                if let data = data , error == nil {
                    do {
                        let appReceiptJSON = try JSONSerialization.jsonObject(with: data)
                        print("success. here is the json representation of the app receipt: \(appReceiptJSON)")
                        // if you are using your server this will be a json representation of whatever your server provided
                    } catch let error as NSError {
                        print("json serialization failed with error: \(error)")
                    }
                } else {
                    print("the upload task returned an error: \(error)")
                }
            }
            task.resume()
        } catch let error as NSError {
            print("json serialization failed with error: \(error)")
        }



    }
}
like image 173
Yasin Aktimur Avatar answered Oct 19 '22 23:10

Yasin Aktimur


I have updated the @user3726962's code, removing unnecessary NS'es and "crash operators". It should look more like Swift 3 now.

Before using this code be warned that Apple doesn't recommend doing direct [device] <-> [Apple server] validation and asks to do it [device] <-> [your server] <-> [Apple server]. Use only if you are not afraid to have your In-App Purchases hacked.

UPDATE: Made the function universal: it will attempt to validate receipt with Production first, if fails - it will repeat with Sandbox. It's a bit bulky, but should be quite self-contained and independent from 3rd-parties.

func tryCheckValidateReceiptAndUpdateExpirationDate() {
    if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
        FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {

        NSLog("^A receipt found. Validating it...")
        GlobalVariables.isPremiumInAmbiquousState = true // We will allow user to use all premium features until receipt is validated
                                                         // If we have problems validating the purchase - this is not user's fault
        do {
            let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
            let receiptString = receiptData.base64EncodedString(options: [])
            let dict = ["receipt-data" : receiptString, "password" : "your_shared_secret"] as [String : Any]

            do {
                let jsonData = try JSONSerialization.data(withJSONObject: dict, options: .prettyPrinted)

                if let storeURL = Foundation.URL(string:"https://buy.itunes.apple.com/verifyReceipt"),
                    let sandboxURL = Foundation.URL(string: "https://sandbox.itunes.apple.com/verifyReceipt") {
                    var request = URLRequest(url: storeURL)
                    request.httpMethod = "POST"
                    request.httpBody = jsonData
                    let session = URLSession(configuration: URLSessionConfiguration.default)
                    NSLog("^Connecting to production...")
                    let task = session.dataTask(with: request) { data, response, error in
                        // BEGIN of closure #1 - verification with Production
                        if let receivedData = data, let httpResponse = response as? HTTPURLResponse,
                            error == nil, httpResponse.statusCode == 200 {
                            NSLog("^Received 200, verifying data...")
                            do {
                                if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject>,
                                    let status = jsonResponse["status"] as? Int64 {
                                        switch status {
                                        case 0: // receipt verified in Production
                                            NSLog("^Verification with Production succesful, updating expiration date...")
                                            self.updateExpirationDate(jsonResponse: jsonResponse) // Leaves isPremiumInAmbiquousState=true if fails
                                        case 21007: // Means that our receipt is from sandbox environment, need to validate it there instead
                                            NSLog("^need to repeat evrything with Sandbox")
                                            var request = URLRequest(url: sandboxURL)
                                            request.httpMethod = "POST"
                                            request.httpBody = jsonData
                                            let session = URLSession(configuration: URLSessionConfiguration.default)
                                            NSLog("^Connecting to Sandbox...")
                                            let task = session.dataTask(with: request) { data, response, error in
                                                // BEGIN of closure #2 - verification with Sandbox
                                                if let receivedData = data, let httpResponse = response as? HTTPURLResponse,
                                                    error == nil, httpResponse.statusCode == 200 {
                                                    NSLog("^Received 200, verifying data...")
                                                    do {
                                                        if let jsonResponse = try JSONSerialization.jsonObject(with: receivedData, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<String, AnyObject>,
                                                            let status = jsonResponse["status"] as? Int64 {
                                                            switch status {
                                                                case 0: // receipt verified in Sandbox
                                                                    NSLog("^Verification succesfull, updating expiration date...")
                                                                    self.updateExpirationDate(jsonResponse: jsonResponse) // Leaves isPremiumInAmbiquousState=true if fails
                                                                default: self.showAlertWithErrorCode(errorCode: status)
                                                            }
                                                        } else { DebugLog("Failed to cast serialized JSON to Dictionary<String, AnyObject>") }
                                                    }
                                                    catch { DebugLog("Couldn't serialize JSON with error: " + error.localizedDescription) }
                                                } else { self.handleNetworkError(data: data, response: response, error: error) }
                                            }
                                            // END of closure #2 = verification with Sandbox
                                            task.resume()
                                        default: self.showAlertWithErrorCode(errorCode: status)
                                    }
                                } else { DebugLog("Failed to cast serialized JSON to Dictionary<String, AnyObject>") }
                            }
                            catch { DebugLog("Couldn't serialize JSON with error: " + error.localizedDescription) }
                        } else { self.handleNetworkError(data: data, response: response, error: error) }
                    }
                    // END of closure #1 - verification with Production
                    task.resume()
                } else { DebugLog("Couldn't convert string into URL. Check for special characters.") }
            }
            catch { DebugLog("Couldn't create JSON with error: " + error.localizedDescription) }
        }
        catch { DebugLog("Couldn't read receipt data with error: " + error.localizedDescription) }
    } else {
        DebugLog("No receipt found even though there is an indication something has been purchased before")
        NSLog("^No receipt found. Need to refresh receipt.")
        self.refreshReceipt()
    }
}

func refreshReceipt() {
    let request = SKReceiptRefreshRequest()
    request.delegate = self // to be able to receive the results of this request, check the SKRequestDelegate protocol
    request.start()
}

This works for auto-renewable subscriptions. Haven't tested it with other kinds of subscriptions yet. Leave a comment if it works for you with some other subscription type.

like image 24
Vitalii Avatar answered Oct 19 '22 22:10

Vitalii