iOS test App Receipt Validation

2019-01-31 20:47发布

问题:

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.

回答1:

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;
}


回答2:

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:

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

}


回答3:

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.