IOS receipt validation error 21002
Asked Answered
D

7

11

I'm trying to use receipt validation with my server side. Everything is ok, but sometimes I see strange: 10 times validation is OK, but on 11 i get 21002 error. I dont know what to do. Sometimes I get error 21002 when I validate receipt first time after launch app.

App side:

func validateReceipt(productID: String) {

    let receipt = NSData(contentsOfURL: NSBundle.mainBundle().appStoreReceiptURL!)!

    let receiptdata = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))

    let request = NSMutableURLRequest(URL: NSURL(string: "my_server_url")!)

    let session = NSURLSession.sharedSession()
    request.HTTPMethod = "POST"

    request.HTTPBody = receiptdata.dataUsingEncoding(NSUTF8StringEncoding)

    let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in

        let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: .MutableLeaves) as? NSDictionary

        if (error != nil) {
            print(error!.localizedDescription)
            let jsonStr = NSString(data: data!, encoding: NSUTF8StringEncoding)
            print("Error could not parse JSON: '\(jsonStr)'")
        }
        else {
            if let parseJSON = json {
                 if String(parseJSON["status"]! == "ok" {
                     //do something
                     print("Validate OK")
                        }else{
                            print("Validate NOK")
                    }
            }
            else {
                let jsonStr = NSString(data: data!, encoding: NSUTF8StringEncoding)
                print("Receipt Error: \(jsonStr)")
            }
        }
    })

    task.resume()
}

server side php script:

function getReceiptData($receipt)
{
$endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';

$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $receipt);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$errmsg = curl_error($ch);
curl_close($ch);
$msg = $response.' - '.$errno.' - '.$errmsg;
echo $response;
}

foreach ($_POST as $key=>$value){
$newcontent .= $key.' '.$value;
}

$new = trim($newcontent);
$new = trim($newcontent);
$new = str_replace('_','+',$new);
$new = str_replace(' =','==',$new);

if (substr_count($new,'=') == 0){
if (strpos('=',$new) === false){
    $new .= '=';
}
}

$new = '{"receipt-data":"'.$new.'"}';
$info = getReceiptData($new);

Everything I do based on example http://www.brianjcoleman.com/tutorial-receipt-validation-in-swift/

So, sometimes I feel that app send to serverside wrong receipt and php script cant parse it and I receive 21002 error status. Any suggestion?

Dashtilut answered 29/9, 2015 at 5:7 Comment(5)
What worked for me was to replace in your PHP code: $new = '{"receipt-data":"'.$new.'"}'; By this: $new = json_encode('{"receipt-data":"'.$new.'"}'); I'm assuming your Swift code is correct.Playpen
@Playpen wrong answer. Did you find the solution?Leffen
Yes I did, my app is validating receipts correctly now. Why do you say my comment/answer is wrong?Playpen
@Heitor: correct me if I'm wrong but I think what you meant was json_encode(["receipt-data" => $new]) instead ... in your comment you're JSON encoding already a JSON stringAnnamarieannamese
Well to be honest I don't remember anymore the context when I posted the comment, but have you tried that code? All I remember now is that the last challenge I faced before my APNS fully started working was an UTF-8 issue probably due to PHP 5.6+ upgrade, when I used json_last_error() to track what was wrong. I hope it can be useful to you.Playpen
K
10

Try removing from receipt the characters '\n' and '\r' and replacing '+' with'%2B' before sending it to the server. Something like this:

 NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
 NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
 NSString *receiptDataString = [receipt base64EncodedStringWithOptions:0];
 receiptDataString=[receiptDataString stringByReplacingOccurrencesOfString:@"+" withString:@"%2B"];
 receiptDataString=[receiptDataString stringByReplacingOccurrencesOfString:@"\n" withString:@""];
 receiptDataString=[receiptDataString stringByReplacingOccurrencesOfString:@"\r" withString:@""];
 NSString *postDataString = [NSString stringWithFormat:@"receipt-data=%@", receiptDataString];
 NSString *length = [NSString stringWithFormat:@"%lu", (unsigned long)[postDataString length]];
 [request setValue:length forHTTPHeaderField:@"Content-Length"];
 [request setHTTPBody:[postDataString dataUsingEncoding:NSASCIIStringEncoding]];
Kathline answered 29/9, 2015 at 7:39 Comment(3)
+1 this is the only answer mentioning replacing + with %2B which is the main culprit ... PHP on the receiving side interprets + as a space (in URL semantics) which breaks the base64 encodingAnnamarieannamese
I can't thank you enough, this seriously saved me! Had spend 3 hours fiddling with what was going on!Hagiology
Encoding the "+" should be provided that you url encode the post parameters. Should you multipart/form-data I think encoding the "+" should be unnecessary and @Jaybo answer should be enoughDanedanegeld
B
7

Code 21002 means that the JSON you are sending to apple which has your shared secret and your receipt data is "misformed" or not in the format apple wants it.

Here is a screenshot with the subsequent error codes and their meaning enter image description here

This is how i did it (Objective C and Local Validation)

  #define kAppReceipt @"LATEST_RECEIPT"
  #define kStoreKitSecret @"YOUR SHARED SECRET"
  #define kSandboxServer @"https://sandbox.itunes.apple.com/verifyReceipt"

-(void)loadProducts{
 NSError *error;

if(![[NSUserDefaults standardUserDefaults]objectForKey:kAppReceipt]){
    NSURL *recieptURL  = [[NSBundle mainBundle]appStoreReceiptURL];
    NSError *recieptError ;
    BOOL isPresent = [recieptURL checkResourceIsReachableAndReturnError:&recieptError];
    if(!isPresent){
        return;
    }

    NSData *recieptData = [NSData dataWithContentsOfURL:recieptURL];
    if(!recieptData){
        return;
    }

    payLoad = [NSMutableDictionary dictionaryWithObject:[recieptData base64EncodedStringWithOptions:0] forKey:@"receipt-data"];
}
else {
    [payLoad setObject:[[NSUserDefaults standardUserDefaults]objectForKey:kAppReceipt] forKey:@"receipt-data"];
}


[payLoad setObject:kStoreKitSecret forKey:@"password"];

NSData *requestData = [NSJSONSerialization dataWithJSONObject:payLoad options:0 error:&error];

NSMutableURLRequest *sandBoxReq = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kSandboxServer]];
[sandBoxReq setHTTPMethod:@"POST"];
[sandBoxReq setHTTPBody:requestData];


NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session dataTaskWithRequest:sandBoxReq completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

    if(!error){
        NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
        NSString * latestReceipt = [jsonResponse objectForKey:@"latest_receipt"];

       // this is the latest receipt that you should store in NSUSER DEFAULT to then later sent this same receipt when you make this same call
        [[NSUserDefaults standardUserDefaults] setObject:latestReceipt forKey:kAppReceipt];
    }

  }] resume];
}
Blameful answered 29/9, 2015 at 5:31 Comment(3)
10 times in row i validate receipt and it's OK, but on 11 it fails. how it could be?Dashtilut
did you implement auto renewable?Blameful
password is not required. :)Itemize
M
6

It's all about the NSDataBase64EncodingOptions. Use type EncodingEndLineWithCarriageReturn instead of 0.

Simply change this line

let receiptdata = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))

to this line

let receiptdata = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.EncodingEndLineWithCarriageReturn)

I tried this myself and it worked.

Microcline answered 22/3, 2016 at 6:53 Comment(3)
I've tried it but it still get above error in sandbox modeLeffen
This solution did work for me, although other aspects of handling string then mangled the receipt in new and different ways. :-)Shortwave
Obective-C: NSString *encodedReceipt = [appReceipt base64EncodedStringWithOptions:(NSDataBase64EncodingEndLineWithCarriageReturn)];Venose
G
2

The Receipt Data is already base64 encoded. Refer to Receipt Validation Programming Guide

$receipt_data = "MII.................KY\/6oc9w==";

$data = "{\"receipt-data\":\"$receipt_data\"}";

$url = "https://buy.itunes.apple.com/verifyReceipt";        // if use this to test sandbox will return "{"status":21007}"
//$url = "https://sandbox.itunes.apple.com/verifyReceipt";  // for sandbox

var_dump(post($url,$data));

function post($url, $data, $headerArray = array())
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST,FALSE);
    curl_setopt($curl, CURLOPT_POST, 1);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    if (array() === $headerArray)
        curl_setopt($curl, CURLOPT_HTTPHEADER,["Content-type:application/json;charset='utf-8'","Accept:application/json"]);

    $output = curl_exec($curl);
    curl_close($curl);
    return $output;
}
Gavrah answered 19/8, 2019 at 13:16 Comment(0)
S
1

This is late so you might well have solved this now - but i noticed a typo - you left out a ")" where you cast to a string in the condition == "ok":

if let parseJSON = json {
    if String(parseJSON["status"]! == "ok" {
    //do something
Szymanski answered 3/3, 2016 at 1:23 Comment(0)
P
0

In my case, I just sent the wrong JSON. Only receipt content.

Predecessor answered 13/12, 2021 at 0:33 Comment(0)
W
0

UPDATE 2024

In the data payload, you must also include your app's shared password.

$data = "{\"password\":\"$PASSWORD\",\"receipt-data\":\"$receipt_data\"}";

$url = "https://sandbox.itunes.apple.com/verifyReceipt"; 

$response = post($url, $data);

function post($url, $data, $headerArray = array())
{
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
    curl_setopt($curl, CURLOPT_POST, 1);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    if (array() === $headerArray)
        curl_setopt($curl, CURLOPT_HTTPHEADER, ["Content-type:application/json;charset='utf-8'", "Accept:application/json"]);

    $output = curl_exec($curl);
    curl_close($curl);
    return $output;
}

Else you can get this error:

data: { environment: 'Sandbox', status: 21003 }
Wormeaten answered 22/3, 2024 at 6:29 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.