iOS test App Receipt Validation
Asked Answered
W

4

25

There is a lot of example about how to test in-app purchase receipt validation by using a sandbox tester account.

But how is the Receipt for the paid App itself? How can we get the App Receipt in development environment?

There is two thing I want to do:

  • To prevent illegal copy of our app running by the user who didn't purchase the app. As I have seen app that detected the iTune Account was connected doesn't owned the app (it shows warning to the user they didn't own the app, but they fail to stop the user to continue to use the app)

  • Send the app purchase receipt to our server. We want to know when do they buy our app, what version of app they brought.

Whisenant answered 1/6, 2016 at 10:51 Comment(0)
R
41

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;
}
Raddle answered 22/9, 2016 at 22:51 Comment(15)
Thank you for this example! I have been searching for quite some time now and this works flawlessly.Avicenna
Thanks for this. Very, very helpful. However, the inexperienced (and I include myself) should note that if you want to pass receipt data to one's own php script on a trusted server, base64 encoded data has to be cleaned up before passing it on to Apple's verification. I am still trying to figure out how to best do it. This isn't an issue for the example above because it passed the request dictionary directly to https://sandbox.itunes.apple.com/verifyReceiptItch
@Murray Sagal Sorry if I'am not right but how it can be production if we test it from the sandbox account with sandbox environment? Or this environment changes if we try it from the real one?Triumvirate
@Triumvirate In production use this url: https://buy.itunes.apple.com/verifyReceiptRaddle
How to change validateAppReceipt for send it to own server?Nether
@Zhanserik See Note 2 in the comments.Raddle
I do not have a backend for my app. If I am using this just to get original_application_version for a paid app, not a IAP or subscription, do I need to validate with apple server?Attainture
@Attainture Yes. You get the receipt like this: let receipt = try Data(contentsOf: receiptURL). So now you have a data buffer. You can't pull original_application_version out of that. There may be a hack to cast it to a dictionary but it's easy to ask apple to do it. Also, just fyi, original_application_version does not return the version number. It returns the build number. So I started adding the version number to my build number. For example, for version 2.0.1 my build number would be 2.0.1.1.Raddle
@Attainture Plus there's no guarantee the app receipt will be present in the bundle. So you have to be prepared to ask Apple for it anyway.Raddle
@MurraySagal. Thanks. Good note on the actual version number. My App Store versions are only major/micro, like 1.12 and then the build number is something like 34. So you're saying I'll get back 34?Attainture
@Attainture Yes. You'll get back "34".Raddle
@MurraySagal. Thanks.Attainture
I am getting 21004 error code. Do I need to add a shared secret in payload? Please help me.Agnew
I am getting below response: The shared secret password is compulsory? { environment = Sandbox; status = 21004; }Underpainting
Yes, you need to add password, generated in iTunes Connect for app or whole account like this: let requestDictionary = ["receipt-data":base64encodedReceipt, "password":"YOUR_PASSWORD_HERE"]Mcroberts
B
6

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:


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. */
    }

}
Beichner answered 1/6, 2016 at 13:20 Comment(10)
So even in development build (never published to public yet), I can still get the App Receipt? (currently we are still in investigating process, so I don't have environment to test)Whisenant
Yes, you will get a validation receipt still in sandbox environment. Basically you'll need to set up an test user account in your iTunes Connect. Then, you need to just make purchase with that credentials and you will get receipt with no financial transactions.Beichner
Refer this linkBeichner
The link is talking about testing In-App purchase, but I am talking about App (paid app) receipt of itself?Whisenant
Receipt of paid App downloaded ?Beichner
Yes, receipt of the paid app downloadedWhisenant
You mean for development build? Because there is App Receipt field developer.apple.com/library/ios/releasenotes/General/…Whisenant
Nope, please scroll to the top, it saids "App Receipt Fields", contains fields: Bundle Identifier, App Version, Opaque Value, SHA-1 Hash, In-App Purchase Receipt, etc. And you can see App Receipt Section and In-App Receipt section on the left.Whisenant
It is the Bundle Identifier, App Version - of the App for which the purchase has been made.Beichner
Sorry if I am not this clear, I am not sure if you are still referring to the In-App Purchase. What I need is the Receipt of the App itself, to prevent illegal copies of the app from running. As described here: developer.apple.com/library/ios/releasenotes/General/…Whisenant
A
2

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

Arbuckle answered 17/12, 2019 at 6:45 Comment(0)
P
0

if you want to test in-app go in the sandbox environment for receipt validation and please take into consideration that in sandbox renewal intervals are

1 week 3 minutes 1 month 5 minutes 2 months 10 minutes 3 months 15 minutes 6 months 30 minutes 1 year 1 hour

The best way is to validate receipt is to communicate your server with the apple server for validation.

Pyxidium answered 28/7, 2017 at 14:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.