4

I have been trying to implement receipt validation in my spritekit game. I have been following various tutorial and basically ended up with this code

enum RequestURL: String {
   case production = "https://buy.itunes.apple.com/verifyReceipt"
   case sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
   case myServer = "my server address"
}

enum ReceiptStatusCode: Int {

// Not decodable status
case unknown = -2

// No status returned
case none = -1

// valid status
case valid = 0

// The App Store could not read the JSON object you provided.
case JSONNotReadable = 21000

// The data in the receipt-data property was malformed or missing.
case malformedOrMissingData = 21002

// The receipt could not be authenticated.
case receiptCouldNotBeAuthenticated = 21003

// The shared secret you provided does not match the shared secret on file for your account.
// Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
case sharedSecretNotMatching = 21004

// The receipt server is currently not available.
case receiptServerUnavailable = 21005

// This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
// Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions.
case subscriptionExpired = 21006

//  This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.
case testReceipt = 21007

// This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.
case productionEnvironment = 21008
 }

  func validateReceipt(forTransaction transaction: SKPaymentTransaction) {

    guard let receiptURL = NSBundle.mainBundle().appStoreReceiptURL else { return }

    guard let receipt = NSData(contentsOfURL: receiptURL) else { return }

    let receiptData = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
    let payload = ["receipt-data": receiptData]

    var receiptPayloadData: NSData?

    do {
        receiptPayloadData = try NSJSONSerialization.dataWithJSONObject(payload, options: NSJSONWritingOptions(rawValue: 0))
    }
    catch let error as NSError {
        print(error.localizedDescription)
        return
    }

    guard let payloadData = receiptPayloadData else { return }
    guard let requestURL = NSURL(string: RequestURL.sandbox.rawValue) else { return }

    let request = NSMutableURLRequest(URL: requestURL)
    request.HTTPMethod = "POST"
    request.HTTPBody = payloadData

    let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
         if let error = error {
            print(error.localizedDescription)
            return
        }  
         guard let data = data else { return }          

         do {
            let jsonData = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves) as? NSDictionary

            guard let json = jsonData else { return }

            // Correct ?
            guard let status = json["status"] as? Int where status == ReceiptStatusCode.valid.rawValue else { return }

            // Unlock product here?
            // Other checks needed?
        }

        catch let error as NSError {
            print(error.localizedDescription)
            return
        }
     }

    task.resume()
}

It is pretty boiler plate code and works as expected. My issue now is that I dont know how to actually validate the receipt at last step (marked line). I believe I have to now carry 5 or so checks to validate the receipt. I just have no idea how most of them would be done in swift. The majority of tutorials are either old, dont include this step or are not written in swift.

If anyone that successfully uses receipt validation could help me get going in the right direction it would be much appreciated. Thank you very much

Update:

After the great answers from JSA986I and cbartel I turned this into a helper on github. Many thanks for the help

https://github.com/crashoverride777/SwiftyReceiptValidator

crashoverride777
  • 10,581
  • 2
  • 32
  • 56
  • Were you able to get this working? If so, would you mind sharing your completed code. I'm fighting the same issue right now. Many thanks! – Robert Dec 09 '15 at 02:42
  • Hey, Unfortunately I have giving up on receipt validation for now. The code above is the correct code minus some changes I made for swift 2 (using guard statements). I just dont understand the last bit of validation, there are very few tutorials out there about this. – crashoverride777 Dec 11 '15 at 11:17
  • Thanks for the reply. I've made a little headway, and will share code here if I'm able to make any more progress. – Robert Dec 12 '15 at 02:45
  • @crashoverride777 - thank you for creating an awesome helper file. Helper works superbly. However it fails with error code 21002 for sandbox testing. After debugging and spending lot of time. Came to know that was happening due to "Paid App" Agreement status is not "Active". If you can document checking status of agreement before using the utility, will definitely help! – Amod Gokhale Mar 25 '21 at 09:09

3 Answers3

10

Here is a solution using Swift version 2.1 following the Apple guide.

Apple also recommends the following:

When validating receipts on your server, your server needs to be able to handle a production-signed app getting its receipts from Apple’s test environment. The recommended approach is for your production server to always validate receipts against the production App Store first. If validation fails with the error code “Sandbox receipt used in production”, validate against the test environment instead.

validateReceipt(NSBundle.mainBundle().appStoreReceiptURL) { (success: Bool) -> Void in
            print(success)
        }

private func receiptData(appStoreReceiptURL : NSURL?) -> NSData? {

    guard let receiptURL = appStoreReceiptURL,
        receipt = NSData(contentsOfURL: receiptURL) else {
            return nil
    }

    do {
        let receiptData = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
        let requestContents = ["receipt-data" : receiptData]
        let requestData = try NSJSONSerialization.dataWithJSONObject(requestContents, options: [])
        return requestData
    }
    catch let error as NSError {
        print(error)
    }

    return nil
}

private func validateReceiptInternal(appStoreReceiptURL : NSURL?, isProd: Bool , onCompletion: (Int?) -> Void) {

    let serverURL = isProd ? "https://buy.itunes.apple.com/verifyReceipt" : "https://sandbox.itunes.apple.com/verifyReceipt"

    guard let receiptData = receiptData(appStoreReceiptURL),
        url = NSURL(string: serverURL)  else {
        onCompletion(nil)
        return
    }

    let request = NSMutableURLRequest(URL: url)
    request.HTTPMethod = "POST"
    request.HTTPBody = receiptData

    let task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in

        guard let data = data where error == nil else {
            onCompletion(nil)
            return
        }

        do {
            let json = try NSJSONSerialization.JSONObjectWithData(data, options:[])
            print(json)
            guard let statusCode = json["status"] as? Int else {
                onCompletion(nil)
                return
            }
            onCompletion(statusCode)
        }
        catch let error as NSError {
            print(error)
            onCompletion(nil)
        }
    })
    task.resume()
}

public func validateReceipt(appStoreReceiptURL : NSURL?, onCompletion: (Bool) -> Void) {

    validateReceiptInternal(appStoreReceiptURL, isProd: true) { (statusCode: Int?) -> Void in
        guard let status = statusCode else {
            onCompletion(false)
            return
        }

        // This receipt is from the test environment, but it was sent to the production environment for verification.
        if status == 21007 {
            self.validateReceiptInternal(appStoreReceiptURL, isProd: false) { (statusCode: Int?) -> Void in
                guard let statusValue = statusCode else {
                    onCompletion(false)
                    return
                }

                // 0 if the receipt is valid
                if statusValue == 0 {
                    onCompletion(true)
                } else {
                    onCompletion(false)
                }

            }

        // 0 if the receipt is valid
        } else if status == 0 {
            onCompletion(true)
        } else {
            onCompletion(false)
        }
    }
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
cbartel
  • 1,298
  • 1
  • 11
  • 18
5

Thats is where you return your receipt as JSON and can access it. ie

if parseJSON["status"] as? Int == 0 {
    println("Sucessfully returned purchased receipt data")
}

Will tell you if you have successfully got the receipt because ["status"] 0 means its been returned ok

You can further query and use the receipt data to find and use items from the JSON response. Here you can print the latest receipt info

if let receiptInfo: NSArray = parseJSON["latest_receipt_info"] as? NSArray {
    let lastReceipt = receiptInfo.lastObject as! NSDictionary
    // Get last receipt
    println("LAST RECEIPT INFORMATION \n",lastReceipt)
}

Now you have your json data to use

You can now query that data, in this example we are finding out when the subscription expires of an auto renew subscription form the JSOn response

// Format date
var formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV"
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")

// Get Expiry date as NSDate
let subscriptionExpirationDate: NSDate = formatter.dateFromString(lastReceipt["expires_date"] as! String) as NSDate!
println("\n   - DATE SUBSCRIPTION EXPIRES = \(subscriptionExpirationDate)")

Post the above code under

if let parseJSON = json {
    println("Receipt \(parseJSON)")
}
pkamb
  • 33,281
  • 23
  • 160
  • 191
JSA986
  • 5,870
  • 9
  • 45
  • 91
  • Omg thank you so much. It has being doing my head in and the information on it is not very clear for me. I knew the "...int == 0" bit but the second code "if let receipt.." is exactly what I was looking for. So I assume if i would like to check the bundle it i would need to write "... parseJSON["bundle_id"] as? NSArray {...." or do i need to use the constant lastReceipt to check the bundle ID? – crashoverride777 Sep 04 '15 at 22:19
  • Its weird the" int == 0" thing works but the" if let receiptInfo" does not work for me, its not calling the println after. Im sorry to trouble you but I feel like i am so close. – crashoverride777 Sep 04 '15 at 22:32
  • 1
    I recomend reading this tutorial http://www.brianjcoleman.com/tutorial-in-app-purchases-iap-in-swift/ and http://www.brianjcoleman.com/tutorial-receipt-validation-in-swift/ the second one is specifically about recipt validation – JSA986 Sep 06 '15 at 19:14
  • thanks I will check it out. I haven't had time recently to check your recommendation. I will mark it as answered as soon as possible. Thank you very much – crashoverride777 Sep 07 '15 at 12:52
  • No problem hope it was useful – JSA986 Sep 07 '15 at 14:36
  • Hey, Thanks again for your help. I have checked the links you send me but they are for subscription items it seems. Can I ask you 1 more question, it seems that the code you provided me is to fetch info for subscriptions. I am trying to do validation on regular non-consumable items. Is that the reason why your suggestion doesnt work? – crashoverride777 Sep 08 '15 at 10:06
  • Fetching a receipt stored by Apple should be the same regardless of what in app purchase your using. Have a look into how you are getting the receipt. It's a bit of a rigmarole but In Order to get the receipt data I mounted a php script on a web server. It sounds complicated but it's explained in the link I gave you and the php script is provided. It's actually not that complicated if you follow the steps – JSA986 Sep 08 '15 at 13:01
0

Today, I have trouble with this problem. And I referenced on this answer. But I discover new way to check subscription is expired or not.

This is my code in Objective-C.

NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:resData options:0 error:&error]; 

// this is response from AppStore
NSDictionary *dictLatestReceiptsInfo = jsonResponse[@"latest_receipt_info"];
long long int expirationDateMs = [[dictLatestReceiptsInfo valueForKeyPath:@"@max.expires_date_ms"] longLongValue];
long long requestDateMs = [jsonResponse[@"receipt"][@"request_date_ms"] longLongValue];
isValidReceipt = [[jsonResponse objectForKey:@"status"] integerValue] == 0 && (expirationDateMs > requestDateMs);

Hope this help.

Clover03ti05
  • 401
  • 1
  • 4
  • 11