Implementing Receipt Validation in Swift 3
Asked Answered
F

6

6

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.

Fries answered 26/9, 2016 at 19:58 Comment(9)
Make sure you have % encoded any + characters in the base64 encoded receipt. Ie replace instances of + with %2bHasid
I changed my code to make this change to the receiptString right after its declaration, but I'm still seeing the same error. Also, when I print out the receiptString, I notice that it contains a lot of "/" characters separating long base 64 strings. Is this how it's supposed to look when correctly encoded?Fries
I should also mention that I tried removing the "/" characters, but I still see the 21002 status.Fries
I updated my Gist to show the code I use to retrieve the receipt and encode the base64 data. In my case this is sent to my php code which then sends it on to Apple's servers gist.github.com/paulw11/fa76e10f785e055338ce06673787c6d2Hasid
Looking at your code, the data you are sending isn't correct. You are sending the receipt and password as POST data, but you need to send a JSON object that contains your receipt and password. If you do that then you shouldn't need to worry about the % encoding, that was something I needed for it to work with my PHP.Hasid
Refer to Apple's example code here - developer.apple.com/library/content/releasenotes/General/… They omit the password value from the JSON (it is only needed when validating auto-renew subscriptions) and is Objective-C but you can see the idea - the POST data is a JSON object that wraps the receiptHasid
I tried basing my code on the example code and changing the receipt and password into a JSON object, but the app hangs if I do this. It seems like the problem I keep running into is that I have to set up the http request differently in Swift 3 than I would in Swift 2 or other languages, but I can't find any examples showing how to do this.Fries
I don't know why but your piece of code worked right away for me. It still has places to work on, but is a great starting point to get the verification working on Swift 3. BTW, in my case it is auto-renewable subscriptions.Umbelliferous
Where put this piece of code? In AppDelegate.swift? Because in my ViewController.swift I can't find a way to make it works.Hayfield
L
13

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



    }
}
Luis answered 6/11, 2017 at 21:45 Comment(4)
Where put this piece of code? In AppDelegate.swift? Because in my ViewController.swift I can't find a way to make it works.Hayfield
It this only for autorenew ? or regular in app purchases too?Burkhart
what is yourpasswordift?Increate
yourpasswordift is your AppSecretKey find it under in-app purchase setting page over iTunes connect.Loos
U
9

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.

Umbelliferous answered 31/1, 2017 at 21:19 Comment(11)
Where put this piece of code? In AppDelegate.swift? Because in my ViewController.swift I can't find a way to make it works.Hayfield
@GhiggzPikkoro Its a self-contained code, can be placed anywhere. In my case it is a part of an IAPHelper class, which in its turn is kept as a static instance of IAPProducts struct named "store". So that from AppDelegate it is called as IAPProducts.store.tryCheckValidateReceiptAndUpdateExpirationDate() whenever I need it.Umbelliferous
Oh yeah I understand, I put it in my IAP service class which I created, and call this method anywhere I want on my object. Thank you it works nowHayfield
Just one last question, when I will submit my app to the App Store, so turns it in Production mode, should I change this following url: sandbox.itunes.apple.com/verifyReceipt by: buy.itunes.apple.com/verifyReceipt. In other way should I change "sandbox" by "buy" in this url before put my app on the AppStore?Hayfield
@GhiggzPikkoro good question. Short answer is YES. But you can also check the updated code in my answer here, it takes care about "Sandbox vs Production" problem, expecting and handling the 21007 code, which means "that receipt is from sandbox, go and check it there, dude". P.S. sorry its still on Swift 3, hope its convertible to 4.Umbelliferous
Ok thanks for your update answer, I will try it, you help me a lotHayfield
Can you please put the complete standalone working code as its making call to refrshReceipt function which is not thereZ
@NikhilPandey, I've added the refreshReceipt function now. But I can't add all my IAP management routines here, 'cause there would be too much code. My code is the answer to particular situation asked in the question, for the IAP implementation in general there are lots of tutorials and posts in the wild prepared by more experienced people than me.Umbelliferous
The quality of your code is very good, rather awesome. Yes its an answer to an only specific question, but it answers a very valid and genuine question and your thinking here is much much better than more experienced people. I urge you to maybe make a GitHub post out of it and post & believe me you'll be surprised by the result. There are functions here like self.updateExpirationDate(jsonResponse: jsonResponse), self.handleNetworkError(data: data, response: response, error: error), which are not there in the base tutorial mentioned as the more experienced author missed it.Z
So making a full code and posting will be great. Just see the tutorial savvyapps.com/blog/… and you'll realize that it has lots of missing functions.Z
And these tutorials are for old versions of Swift and IAP and undergone a major change in the last 1-2 years. In fact, its most changing concept and API and implementing it requires JSON parsing, saving data, Networking and to have your own custom server for receipt validation (the Apple recommended way) but still on Github or on this site you'll not find anything, which answers all of these comprehensively at one point, so a posting like this will definitely help.Z
G
5

//too low rep to comment

Yasin Aktimur, thanks for your answer, it's awesome. However, looking at Apple documentation on this, they say to connect to iTunes on a separate Queue. So it should look like this:

func receiptValidation() {

    let SUBSCRIPTION_SECRET = "secret"
    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 base64encodedReceipt = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.endLineWithCarriageReturn)
        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 queue = DispatchQueue(label: "itunesConnect")
            queue.async {
                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, options: .allowFragments) as? NSDictionary
                            print("success. here is the json representation of the app receipt: \(appReceiptJSON)")    
                        } catch let error as NSError {
                            print("json serialization failed with error: \(error)")
                        }
                    } else {
                        print("the upload task returned an error: \(error ?? "couldn't upload" as! Error)")
                    }
                }
                task.resume()
            }

        } catch let error as NSError {
            print("json serialization failed with error: \(error)")
        }
    }
}
Greenstein answered 17/1, 2018 at 18:32 Comment(3)
Where put this piece of code? In AppDelegate.swift? Because in my ViewController.swift I can't find a way to make it works.Hayfield
Ok what should I ask to my friend Google, help me to do a sentence, my english is so bad and translator can't find a way to help meHayfield
Will you be calling the function from more than one class? If so make this function public in a separate file to keep everything clean. If not than just put it in your class and call it once a purchase is madeGreenstein
D
2

I struggled my head with the same problem. The issue is that this line:

let receiptString = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))

Returns an OPTIONAL and

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

cannot handle optionals. So to fix it, simply substitute the first line of code with this:

let receiptString:String = receiptData?.base64EncodedString(options: NSData.Base64EncodingOptions.lineLength64Characters) as String!

And everything will work like charm!

Dislimn answered 13/10, 2016 at 16:5 Comment(6)
This is odd, but your solution returns error 21002 while the original code given in the question works perfectly (even despite receiptString being an optional). I'm not downvoting because there might be some subtle differences depending on purchase types or anything else I'm not yet aware about, so that someone could still probably find this answer helpful.Umbelliferous
@VitaliiTymoshenko at the original question the receiptData is an optional and it cannot be passed to JSONSerialization.data(...). Maybe your problem is another one.Dislimn
Your suggested code uses .lineLength64Characters. I have double-checked that getting String from Data with this option produces error 21002 for me. I agree that original code had Optional (i have clearly seen it while debugging) but I was also surprised that it worked correctly for me somehow. So I ended up using the default encoding method - receiptData.base64EncodedString(options: []). See the full code in my answer below.Umbelliferous
Where put this piece of code? In AppDelegate.swift? Because in my ViewController.swift I can't find a way to make it works.Hayfield
@GhiggzPikkoro read original question. Original code was at: savvyapps.com/blog/…Dislimn
this is a big waste of timeSciamachy
G
1

I liked your answer and I just rewrote it in C# for those who are using it like me as I did not find a good source for the solution. Thanks Again For Consumable IAP

void ReceiptValidation()
    {
        var recPath = NSBundle.MainBundle.AppStoreReceiptUrl.Path;
        if (File.Exists(recPath))
        {
            NSData recData;
            NSError error;

            recData = NSData.FromUrl(NSBundle.MainBundle.AppStoreReceiptUrl, NSDataReadingOptions.MappedAlways, out error);

            var recString = recData.GetBase64EncodedString(NSDataBase64EncodingOptions.None);

            var dict = new Dictionary<String,String>();
            dict.TryAdd("receipt-data", recString);

            var dict1 = NSDictionary.FromObjectsAndKeys(dict.Values.ToArray(), dict.Keys.ToArray());
            var storeURL = new NSUrl("https://sandbox.itunes.apple.com/verifyReceipt");
            var storeRequest = new NSMutableUrlRequest(storeURL);
            storeRequest.HttpMethod = "POST";

            var jsonData = NSJsonSerialization.Serialize(dict1, NSJsonWritingOptions.PrettyPrinted, out error);
            if (error == null)
            {
                storeRequest.Body = jsonData;
                var session = NSUrlSession.FromConfiguration(NSUrlSessionConfiguration.DefaultSessionConfiguration);
                var tsk = session.CreateDataTask(storeRequest, (data, response, err) =>
                {
                    if (err == null)
                    {
                        var rstr = NSJsonSerialization.FromObject(data);

                    }
                    else
                    {
                        // Check Error
                    } 
                });
                tsk.Resume();
            }else
            {
                // JSON Error Handling
            }
        }
    }
Gyroscope answered 2/8, 2018 at 11:13 Comment(0)
F
0

Eventually I was able to solve the problem by having my app call a Lambda function written in Python, as shown in this answer. I'm still not sure what was wrong with my Swift code or how to do this entirely in Swift 3, but the Lambda function got the desired result in any case.

Fries answered 1/10, 2016 at 3:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.