Episode #45

Validating IAP Receipts

12 minutes
Published on December 13, 2012

This video is only available to subscribers. Get access to this video and 572 others.

Here we continue on with our In App Purchase example, but this time we take the receipt given to us by StoreKit and we send it to our custom rails server to be validated with Apple.

Episode Links

Setting up a Rails app to validate receipts

We start by creating our rails app:

rails new iap_server

Next, we open the Gemfile and add the itunes-receipt gem to it. Run bundle install to get the new gem installed.

We'll need a route to serve as our endpoint, so open up the routes.rb file and add this line:

post 'receipts/validate' => 'receipts#validate'

Finally, we need to add our controller:

class ReceiptsController < ApplicationController
  def validate
    receipt_data = params[:receipt_data]
    receipt = Itunes::Receipt.verify! receipt_data, :allow_sandbox
    render :json => {:status => "ok", :receipt => receipt}
  rescue StandardError => e
    render :json => {:status => "error", :message => e.message}, :status => 400
  end
end

Note that the :allow_sandbox parameter is just a "truthy" value. This is done to make the code more obvious

Now run the server with rails s.

On to the iOS App

We need to add the NSData+Base64 pod to our Podfile to get base 64 encoding support for NSData. Run pod install to get this into the project.

Next, we'll need to open up our IAPGateway.m and make some modifications. We'll add a couple of things to the top of the file:

#import "NSData+Base64.h"

typedef void (^IAPGatewayReceiptValidateBlock)(BOOL ok, id receipt);

Next, we define a method to do the validation. Here we're just using the built-in NSURL Loading system.

- (void)validateReceipt:(NSData *)receiptData completion:(IAPGatewayReceiptValidateBlock)completion {
    NSURL *url = [NSURL URLWithString:@"http://localhost:3000/receipts/validate"];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    [request setHTTPMethod:@"POST"];

    NSString *params = [NSString stringWithFormat:@"receipt_data=%@", [receiptData base64EncodedString]];
    NSData *httpBody = [params dataUsingEncoding:NSUTF8StringEncoding];
    [request setHTTPBody:httpBody];

    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                               NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
                               if (httpResponse.statusCode == 200) {
                                   id receipt = [NSJSONSerialization JSONObjectWithData:data
                                                                                options:0 error:nil];
                                   NSLog(@"Received receipt: %@", receipt);
                                   completion(YES, receipt);
                               } else {
                                   NSLog(@"Body: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
                                   NSLog(@"ERROR: %@", error);
                                   NSLog(@"HTTP STATUS: %d", httpResponse.statusCode);
                                   completion(NO, nil);
                               }
                           }];
}

Now we just need to tie this into our existing transaction updated callback:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased: {
                [self validateReceipt:transaction.transactionReceipt
                           completion:^(BOOL ok, id receipt) {
                               if (ok) {
                                 [self markProductPurchased:transaction.payment.productIdentifier];
                                 [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                                } else {  /* handle failure */ }
                           }];
            }
                break;

            case SKPaymentTransactionStateFailed:
                NSLog(@"Transaction failed: %@", transaction.error.localizedDescription);
                // raise notification?
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;

            case SKPaymentTransactionStateRestored: {
                [self validateReceipt:transaction.transactionReceipt
                           completion:^(BOOL ok, id receipt) {
                               if (ok) {
                                 [self markProductPurchased:transaction.originalTransaction.payment.productIdentifier];
                                 [[SKPaymentQueue defaultQueue] finishTransaction:transaction];                               
                               } else { /* handle failure */ }
                           }];
                break;
            }

            default:
                break;
        }
    }
}

Now, instead of just recording the product id into NSUserDefaults, you'll also want to save the transaction id. I also recommend saving the receipts on the server so that you can easily verify them whenever you need to based on the transaction id.