Verify receipt for in App purchase

China☆狼群 提交于 2019-11-26 15:37:14
Joe D'Andrea

First, there are a few typos in the posted code. Try this. (Disclaimer: Refactoring et. al is left as an exercise for the readership!)

- (BOOL)verifyReceipt:(SKPaymentTransaction *)transaction {
    NSString *jsonObjectString = [self encode:(uint8_t *)transaction.transactionReceipt.bytes length:transaction.transactionReceipt.length];      
    NSString *completeString = [NSString stringWithFormat:@"http://url-for-your-php?receipt=%@", jsonObjectString];               
    NSURL *urlForValidation = [NSURL URLWithString:completeString];       
    NSMutableURLRequest *validationRequest = [[NSMutableURLRequest alloc] initWithURL:urlForValidation];              
    [validationRequest setHTTPMethod:@"GET"];         
    NSData *responseData = [NSURLConnection sendSynchronousRequest:validationRequest returningResponse:nil error:nil];  
    [validationRequest release];
    NSString *responseString = [[NSString alloc] initWithData:responseData encoding: NSUTF8StringEncoding];
    NSInteger response = [responseString integerValue];
    [responseString release];
    return (response == 0);
}

- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length {
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
    uint8_t *output = (uint8_t *)data.mutableBytes;

    for (NSInteger i = 0; i < length; i += 3) {
        NSInteger value = 0;
        for (NSInteger j = i; j < (i + 3); j++) {
            value <<= 8;

            if (j < length) {
                value |= (0xFF & input[j]);
            }
        }

        NSInteger index = (i / 3) * 4;
        output[index + 0] =                    table[(value >> 18) & 0x3F];
        output[index + 1] =                    table[(value >> 12) & 0x3F];
        output[index + 2] = (i + 1) < length ? table[(value >> 6)  & 0x3F] : '=';
        output[index + 3] = (i + 2) < length ? table[(value >> 0)  & 0x3F] : '=';
    }

    return [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease];
}

You can make these Internal methods on the class that handles your SKPaymentTransactionObserver messages:

@interface YourStoreClass (Internal)
- (BOOL)verifyReceipt:(SKPaymentTransaction *)transaction;
- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length;
@end

Note: You could use something like libcrypto to handle base64 encoding, but then you're looking at export restrictions and extra steps at app approval time. But I digress ...

Then, wherever you intend to kick-off recording the transaction on your remote server, call verifyReceipt: with your transaction and make sure it comes back positive.

Meanwhile, on your server, here's some super-stripped-down PHP to handle things:

$receipt = json_encode(array("receipt-data" => $_GET["receipt"]));
// NOTE: use "buy" vs "sandbox" in production.
$url = "https://sandbox.itunes.apple.com/verifyReceipt";
$response_json = call-your-http-post-here($url, $receipt);
$response = json_decode($response_json);

// Save the data here!

echo $response->status;

Where call-your-http-post-here is your favorite HTTP post mechanism. (cURL is one possible choice. YMMV. PHP.net has the scoop!)

One thing that has me slightly concerned is the length of the payload in the URL going from the app to the server (via GET). I forget if there's a length issue there per the RFCs. Maybe it's OK, or maybe it's server-specific. (Readers: Advisement welcome on this part!)

There may also be some balking at making this a synchronous request. You may want to post it asynchronously and put up the ol' UIActivityIndicatorView or some other HUD. Case in point: That initWithData:encoding: call takes a loooooong time for me. A few seconds, which is a small eternity in iPhone land (or anywhere else online, for that matter). Showing some sort of indeterminate progress indicator may be advisable.

Chris Maddern

The full source code, as well as a hosted example of a PHP implementation is available at: http://www.chrismaddern.com/validate-app-store-iap-receipt-codes-online-tool/

Hope it helps you!

For anyone who's wondering how to handle connection or verification errors that might occur when you're using the In-App-Purchase server model. Receipt validation ensures that the transaction is complete and successful. You don't want to do that from the iPhone because you can't really trust the user's phone.

  1. The user initiates an in-app purchase
  2. When complete, the app asks your server for validation
  3. You validate the receipt with Apple: if it's valid, you can perform whatever action linked to the purchase (unlock/deliver content, register subscription...)
  4. The app removes the transaction from the queue (finishTransaction)

If the server is down, you shouldn't finish the transaction, but display an "unavailability message" to the user.

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions

will be called again later.

But if you find out that a receipt is invalid, you should finish the associated transaction. If not, you may have extra-transactions living forever in the transaction queue. That means that each time your app runs, paymentQueue:updatedTransaction: will be called once per transaction...

In my apps, receipt validation is done through a web service, returning an error code in case of an invalid receipt. That's why an external server is needed. If a user somehow manages to skip receipt validation (by faking the web service "success" response), he won't be able to unlock the content / access functionality because the server has no trace of the purchase.

After fighting with this for awhile, I finally found a listing of status codes in Apple's documentation, including the dreaded 21002 (which is "The data in the receipt-data property was malformed."). While I've seen reports of other status codes not included in this list, I have thus far not seen any beyond what Apple has documented. Note that these codes are only valid for auto-renew subscriptions, not other sorts of in-app purchases (or so the document says).

The document in question can be found here.

I'm surprised to not have found Ray Wenderlich's tutorial here - just saved my life. Goes through validating receipts without a server (not a recommended solution but heavily done anyways).

http://www.raywenderlich.com/23266/in-app-purchases-in-ios-6-tutorial-consumables-and-receipt-validation

You must send the receipt as a file to your PHP server. In your PHP side you can use this script to validate:

<?php

$path = 'receipt'; // $_FILE['receipt-data']["tmp_name"];
$receipt = file_get_contents($path);

$json['receipt-data'] = base64_encode($receipt);

$post = json_encode($json);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,"https://buy.itunes.apple.com/verifyReceipt");
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
$result=curl_exec ($ch);

curl_close ($ch);

?>

https://gist.github.com/eduardo22i/9adc2191f71ea612a7d071342e1e4a6f

Just to open this again and add my 2-cents in return for scourging these forms for information.

I just setup an IAP service in my app and ran into the same 21002 error. I found the 21002 happens when either the post to your PHP server is empty (thus the HTTP request to the app store is empty) or improperly formatted. To get ours working, on the iPhone side we set the post data in a NSString as base64 encoded then sent it to our server as a HTTP request.

Then on our server, we stuck it into and array and json-ed it. Like this:

$receipt = json_encode(array("receipt-data"=>$_POST['receipt-data']));

You'll notice it is the same as above except we are using a POST instead of a GET. Personal preference really.

We then used CURL to post it to the sandbox and used json_decode on the response.

This is a great library that do what anyone will need about this subject:

https://github.com/aporat/store-receipt-validator

It goes beyond and validates:

  • iTunes
  • Play Store
  • Amazon App Store

I hope it helps someone more like is helping me.

If you are getting null responses or error codes eg 21002, try adding these lines. If you checked the curl error codes, it is a SSL certificate error...

curl_setopt ($curl_handle, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt ($curl_handle, CURLOPT_SSL_VERIFYPEER, 0);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!