iOS test App Receipt Validation

I am assuming that you know how to perform InApp purchase.

We are required to validate a receipt, after a transaction is finished.

- (void)completeTransaction:(SKPaymentTransaction *)transaction 
{
    NSLog(@"completeTransaction...");
    
    [appDelegate setLoadingText:VALIDATING_RECEIPT_MSG];
    [self validateReceiptForTransaction];
}

Once the product has been purchased successfully, it needs to be validated. Server does this for us, we just need to pass Receipt data returned by Apple server.

-(void)validateReceiptForTransaction
{
    /* Load the receipt from the app bundle. */
    
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    
    if (!receipt) { 
        /* No local receipt -- handle the error. */
    }
    
    /* ... Send the receipt data to your server ... */
    
    NSData *receipt; // Sent to the server by the device
    
    /* Create the JSON object that describes the request */
    
    NSError *error;
    
    NSDictionary *requestContents = @{ @"receipt-data": [receipt base64EncodedStringWithOptions:0] };
    
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];
    
    if (!requestData) { 
        /* ... Handle error ... */ 
    }
    
    // Create a POST request with the receipt data.
    
    NSURL *storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];
    
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];
    
    /* Make a connection to the iTunes Store on a background queue. */
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                               
                               if (connectionError) {
                                   /* ... Handle error ... */
                               } 
                               else {
                                   NSError *error;
                                   NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                                   
                                   if (!jsonResponse) { 
                                       /* ... Handle error ...*/ 
                                   }
                                   
                                   /* ... Send a response back to the device ... */
                               }
                           }];
}

The response’s payload is a JSON object that contains the following keys and values:

status:

Either 0 if the receipt is valid, or one of the error codes mentioned below:

enter image description here

For iOS 6 style transaction receipts, the status code reflects the status of the specific transaction’s receipt.

For iOS 7 style app receipts, the status code is reflects the status of the app receipt as a whole. For example, if you send a valid app receipt that contains an expired subscription, the response is 0 because the receipt as a whole is valid.

receipt:

A JSON representation of the receipt that was sent for verification.

Remember:

  • We will get staus code 21007 for successful receipt valication, in Sandbox environment.

  • In the test environment, use https://sandbox.itunes.apple.com/verifyReceipt as the URL. In production, use https://buy.itunes.apple.com/verifyReceipt as the URL.

  • You will need to set up an test user account in your iTunes Connect to test purchase in sandbox environment.


EDIT 1

transactionReceipt is deprecated: first deprecated in iOS 7.0

if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) {
    // iOS 6.1 or earlier.
    // Use SKPaymentTransaction's transactionReceipt.

} else {
    // iOS 7 or later.

    NSURL *receiptFileURL = nil;
    NSBundle *bundle = [NSBundle mainBundle];
    if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) {

        // Get the transaction receipt file path location in the app bundle.
        receiptFileURL = [bundle appStoreReceiptURL];

        // Read in the contents of the transaction file.

    } else {
        /* Fall back to deprecated transaction receipt,
           which is still available in iOS 7.
           Use SKPaymentTransaction's transactionReceipt. */
    }

}

Most parts of the answer can be found here in Apple's documentation. But there are gaps and the objective-c code is using deprecated methods.

This Swift 3 code shows how to get the App Receipt and send it to the app store for validation. You should definitely validate the App Receipt with the app store before saving the data you want. The advantage of asking the app store to validate is that it responds with data that you can easily serialize to JSON and from there pull out the values for the keys you want. No cryptography required.

As Apple describes in that documentation the preferred flow is like this...

device -> your trusted server -> app store -> your trusted server -> device

When the app store returns to your server, assuming success, that's where you'll serialize and pull out the data you require and save it as you wish. See the JSON below. And you can send the result and whatever else you want back to the app.

In validateAppReceipt() below, to make it a working example, it simply uses this flow...

device -> app store -> device

To make this work with your server just change validationURLString to point to your server and add whatever else your require to requestDictionary.

To test this in development you need to:

  • make sure you have a sandbox user set up in itunesconnect
  • on your test device sign out of iTunes & App Store
  • during testing, when prompted, use your sandbox user

Here's the code. The happy path flows just fine. Errors and failure points just print or are commented. Deal with those as you require.

This part grabs the app receipt. If it's not there (which will happen when you are testing) it asks the app store to refresh.

let receiptURL = Bundle.main.appStoreReceiptURL

func getAppReceipt() {
    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)
        validateAppReceipt(receipt)
    } 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.
    }
}

func requestDidFinish(_ request: SKRequest) {
    // a fresh receipt should now be present at the url
    do {
        let receipt = try Data(contentsOf: receiptURL!) //force unwrap is safe here, control can't land here if receiptURL is nil
        validateAppReceipt(receipt)
    } catch {
        // still no receipt, possible but unlikely to occur since this is the "success" delegate method
    }
}

func request(_ request: SKRequest, didFailWithError error: Error) {
    print("app receipt refresh request did fail with error: \(error)")
    // for some clues see here: https://samritchie.net/2015/01/29/the-operation-couldnt-be-completed-sserrordomain-error-100/
}

This part validates the app receipt. This is not local validation. Refer to Note 1 and Note 2 in the comments.

func validateAppReceipt(_ receipt: Data) {

    /*  Note 1: This is not local validation, the app receipt is sent to the app store for validation as explained here:
            https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW1
        Note 2: Refer to the url above. For good reasons apple recommends receipt validation follow this flow:
            device -> your trusted server -> app store -> your trusted server -> device
        In order to be a working example the validation url in this code simply points to the app store's sandbox servers.
        Depending on how you set up the request on your server you may be able to simply change the 
        structure of requestDictionary and the contents of validationURLString.
    */
    let base64encodedReceipt = receipt.base64EncodedString()
    let requestDictionary = ["receipt-data":base64encodedReceipt]
    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)")
    }
}

You should end up with something like this. In your case this is what you would be working with on your server.

{
    environment = Sandbox;
    receipt =     {
        "adam_id" = 0;
        "app_item_id" = 0;
        "application_version" = "0";  // for me this was showing the build number rather than the app version, at least in testing
        "bundle_id" = "com.yourdomain.yourappname";  // your app's actual bundle id
        "download_id" = 0;
        "in_app" =         (
        );
        "original_application_version" = "1.0"; // this will always return 1.0 when testing, the real thing in production.
        "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
        "original_purchase_date_ms" = 1375340400000;
        "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
        "receipt_creation_date" = "2016-09-21 18:46:39 Etc/GMT";
        "receipt_creation_date_ms" = 1474483599000;
        "receipt_creation_date_pst" = "2016-09-21 11:46:39 America/Los_Angeles";
        "receipt_type" = ProductionSandbox;
        "request_date" = "2016-09-22 18:37:41 Etc/GMT";
        "request_date_ms" = 1474569461861;
        "request_date_pst" = "2016-09-22 11:37:41 America/Los_Angeles";
        "version_external_identifier" = 0;
    };
    status = 0;
}

It works for iOS 13

Here's the steps to verify reciept on device without any server code:

You need password before verifying the receipt. It would be the shared secret key.

How to generate it:

Go to -> iTunes connect go into "Contracts, Tax, and Banking" and click "Request" on the iOs paid apps contract, then accept the contract.

Visit this link

https://appstoreconnect.apple.com

1:- Click on Features

2:- Click on In-App Purchases and create your subscription package

3:- After creating successfully subscription click on App-Specific Shared Secret

4:- Generate App-Specific Shared Secret


Updated code to verify receipt for subscription in-app:

-(void) verifyReceipt
{
/* Load the receipt from the app bundle. */

NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

if (!receipt) {
    /* No local receipt -- handle the error. */
}

/* Create the JSON object that describes the request */
NSError *error;

/* reciept data and password to be sent, password would be the Shared Secret Key from Apple Developer account for given app. */
NSDictionary *requestContents = @{
                                  @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                 ,@"password": @"2008687bb49145445457ff2b25e9bff3"};

NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                      options:0
                                                        error:&error];

if (!requestData) {
    /* ... Handle error ... */
}

// Create a POST request with the receipt data.
NSURL *storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];

NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];

/* Make a connection to the iTunes Store on a background queue. */
//NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:storeRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    // handle request error
    if (error) {
        //completion(nil, error);
        return;
    } else {
        NSError *error;
        NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

        if (!jsonResponse) {
            /* ... Handle error ...*/
        }

        /* ... Send a response back to the device ... */
    }
}];
[dataTask resume];
}

Hope that helps

Thanks