4

I'm having an issue validating receipts for an auto-renewing IAP. Below is a summary of my code for verifying subscriptions. I'm using Parse for a backend, and have some of that code included also. In development mode, everything works perfectly and has no issues. It's live in the App Store, and all of my TestFlight testers, and some real users are experiencing crashes as soon as the app tries to validate. I'm seeing that the crash is coming from the very end of the function when I try to save data back to Parse, it's telling me the keys I'm saving are null. (base64, info, expirationDate)

On my device, I had a sandbox purchase receipt and I get a 21007 response from Apple trying to validate against live URL. When I switch to sandbox url, it validates and works every time.

Also, I know it's not safe to validate purchases this exact way, I perform validation via my server, so no issue there.

I'm trying to figure out if I'm missing a step of if there's something I should be doing differently?

Pseudo code:

Get the receipt from NSBundle
Get the receipt from Parse database
If neither of them exist:
    end the function
else:
    if Parse receipt exists:
        use it, but first just check the expirationDate stored in Parse
    else:
        use NSBundle receipt for validation, 
    If expired based on date from Parse:
        build request to send to Apple (my server in production)
        Get JSON response, and perform switch statement for response codes
    Check for errors or expiration
    Save new data to Parse // <-- Cause of the crash is here because the keys are null for some users

Here's a piece of actual code:

PFUser *user = [PFUser currentUser];

//Load the receipt from the app bundle
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

//Load the receipt from Parse, already encoded
NSString *saved64 = user[@"base64"];

//Check for at least one instance of a receipt... Either Parse or appBundle
if (!receipt && saved64.length == 0) {
//if (!receipt) {
    //No receipt
    NSLog(@"No Receipt");
    return;
}

//Base 64 encode appBundle receipt
NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];

//String to hold base64 receipt (either from Parse or appBundle)
NSString *temp64;

//See if Parse base64 exists
if (saved64.length == 0) {
    //Not a receipt in Parse yet, use appBundle
    NSLog(@"Using appBundle receipt.");
    temp64 = receipt64;
} else {
    //Receipt in Parse, use it
    NSLog(@"Using Parse receipt.");
    temp64 = saved64;

    //Check expiration date stored in Parse
    NSDate *parseExpDate = user[@"expirationDate"];
    if ([[self todayGMT] compare:parseExpDate] == NSOrderedAscending) {
        //Active based on Parse, no need to validate...
        NSLog(@"Active based on Parse receipt... No need to validate!");
        return;
    }
}

    //Base 64 encode appBundle receipt
    NSString *receipt64 = [receipt base64EncodedStringWithOptions:0];


    //Create the request
    NSString *sharedSecret = @"[shared-secret]";
    NSError *error;
    //Request with receipt data and shared secret from iTunesConnect
    NSDictionary *requestContents = @{@"receipt-data":receipt64, @"password": sharedSecret};
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
    if (!requestData) {
        //Handle error
        NSLog(@"Error: %@", error);

        return;
    }

    //Create a POST request with the receipt data.
    NSURL *storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];                      
    //NSURL *sandboxURL = [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
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (connectionError) {
            //Error
        } else {
            //Success
            NSError *error;
            NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
            if (!jsonResponse) {
                //Error
                NSLog(@"Error at !jsonResponse: %@", error);
                return;
            }

            NSString *base64 = jsonResponse[@"latest_receipt"];
            NSArray *info = jsonResponse[@"latest_receipt_info"];
            BOOL isPro;

            //Switch statement for subscription status
            switch ([jsonResponse[@"status"] intValue]) {
                case 21002: {
                    //The data in the receipt-data property was malformed or missing.
                    NSLog(@"21002 : The data in the receipt-data property was malformed or missing.");
                    isPro = NO;
                    break;
                }
                case 21003: {
                    //The receipt could not be authenticated.
                    NSLog(@"21003 : The receipt could not be authenticated.");
                    isPro = NO;
                    break;
                }
                case 21004: {
                    //The shared secret you provided does not match the shared secret on file for your account.
                    NSLog(@"21004 : The shared secret you provided does not match the shared secret on file for your account.");
                    isPro = NO;
                    break;
                }
                case 21005: {
                    //The receipt server is not currently available.
                    NSLog(@"21005 : The receipt server is not currently available.");
                    isPro = NO;
                    break;
                }
                case 21006: {
                    //This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
                    NSLog(@"21006 : This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.");
                    isPro = NO;
                    break;
                }
                case 21007: {
                    //This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
                    NSLog(@"21007 : This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead..");
                    isPro = NO;
                    break;
                }
                case 21008: {
                    //This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response.
                    NSLog(@"21008 : This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead..");
                    isPro = NO;
                    break;
                }
                case 0: {
                    //Valid and active
                    NSLog(@"0 : Valid and active subscription.");
                    isPro = YES;
                    break;
                }
                default: {
                    isPro = NO;
                    break;
                }
            }

            //Set user info to database (Parse)
            user[@"base64"] = base64;
            user[@"info"] = info;
            user[@"expirationDate"] = expirationDate;
            user[@"isPro"] = [NSNumber numberWithBool:isPro];
            [user saveEventually];
        }
    }];
Erik S
  • 1,939
  • 1
  • 18
  • 44
tcd
  • 1,585
  • 2
  • 17
  • 38
  • I'm working on a similar problem. What did you figure out for this? – blwinters Jun 03 '15 at 02:57
  • @blwinters for some weird reason, sometimes `base64`, `info` and `expirationDate` would be empty... So my solution was to check if they exist prior to saving. They have no impact on checking if the user's subscription, it still returns the correct subscription status. Message me to talk more. – tcd Jun 05 '15 at 02:58
  • Okay, that makes sense, thanks. I use Swift with optional binding on all data being saved to Parse, so that should work. – blwinters Jun 05 '15 at 03:06
  • Yea, that will work! – tcd Jun 05 '15 at 03:22

1 Answers1

0

Sandbox URL - https://sandbox.itunes.apple.com/verifyReceipt

Sandbox URL works in developing mode only with the developer certificate, So it gets the response from a server.

Live URL - https://buy.itunes.apple.com/verifyReceipt

Live URL works in distribution mode only with the distribution certificate, So it didn't work in developing mode and couldn't return the response.

So if you want to use the live URL with the debug mode, you should handle the exceptions.

If you use Swift with optional binding, you can parse without crashes.

Ramkumar Paulraj
  • 1,841
  • 2
  • 20
  • 40