Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS test App Receipt Validation

Tags:

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.

like image 847
King Chan Avatar asked Jun 01 '16 10:06

King Chan


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.

How do you validate in-app purchases?

When a user makes an in-app purchase, cache the details (token, order id, and product id) locally on the client (i.e the app) then send it to your API. Your API should then send the purchaseToken to the Google Play Developer API for validation.

How do I validate my apps on iPhone?

To use the app, connect to the Internet and tap the Verify App button. After you verify an app for the first time, your iPhone, iPad, or iPod touch must reverify the app developer's certificate periodically to maintain trust. If you can't reverify, you may see a message that verification will expire soon.


2 Answers

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; } 
like image 58
Murray Sagal Avatar answered Sep 19 '22 05:09

Murray Sagal


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

}
like image 26
NSPratik Avatar answered Sep 22 '22 05:09

NSPratik