I created a class that handles in-app purchases, as well as check checks. Some time ago I used the transactionReceipt property in SKPaymentTransaction, but I updated my code sufficiently and now I use appStoreReceiptURL in [NSBundle mainBundle].
Basically, it seems that my receipt is sent to the Apple server in an acceptable way, but I continue to receive the status code 21002. In automatic renewable subscriptions, I know that this means that the receipt is not in an acceptable format, but I have no idea what this means status regarding the in-app purchase receipt.
Here is the local method to confirm receipt:
- (void)validateReceiptForTransaction:(SKPaymentTransaction *)transaction
{
IAPProduct *product = self.internalProducts[transaction.payment.productIdentifier];
NSData *receiptData = [[NSData alloc] initWithContentsOfURL:[NSBundle mainBundle].appStoreReceiptURL];
NSString *receipt = [receiptData base64EncodedStringWithOptions:kNilOptions];
NSLog(@"Receipt: %@", receipt);
NSURL *verificationURL = [[NSURL alloc] initWithString:IAPHelperServerBaseURL];
verificationURL = [verificationURL URLByAppendingPathComponent:IAPHelperServerReceiptVerificationComponent];
NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:verificationURL];
urlRequest.HTTPMethod = @"POST";
NSDictionary *httpBody = @{@"receipt" : receipt,
@"sandbox" : @(1)};
urlRequest.HTTPBody = [NSKeyedArchiver archivedDataWithRootObject:httpBody];
[NSURLConnection sendAsynchronousRequest:urlRequest
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
void (^failureBlock)(NSString *failureMessage) = ^void(NSString *failureMessage)
{
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
NSLog(@"%@", failureMessage);
if (self.transactionToValidate)
product.purchaseInProgress = NO,
[[SKPaymentQueue defaultQueue] finishTransaction:transaction],
[self notifyStatus:@"Validation failed." forProduct:product],
self.transactionToValidate = nil;
else
self.transactionToValidate = transaction,
[self refreshReceipt];
}];
};
if (connectionError)
{
failureBlock([[NSString alloc] initWithFormat:@"Failure connecting to server: %@", connectionError]);
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSError *jsonError;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
if (!json)
{
NSString *responseString = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
NSString *failureMessage = [[NSString alloc] initWithFormat:@"Failure parsing JSON: %@\nServer Response: %@ (%@)",
data, responseString, @(httpResponse.statusCode)];
failureBlock(failureMessage);
return;
}
NSInteger statusCode = [json[@"status"] integerValue];
NSString *errorDescription = json[@"error"];
if (statusCode != 0)
{
NSString *failureMessage = [[NSString alloc] initWithFormat:@"Failure verifying receipt: %@", errorDescription];
failureBlock(failureMessage);
}
else
NSLog(@"Successfully verified receipt."),
[self provideContentForCompletedTransaction:transaction productIdentifier:transaction.payment.productIdentifier];
}];
}
An important PHP function on the server does this:
function validateReceipt($receipt, $sandbox)
{
if ($sandbox)
$store = 'https://sandbox.itunes.apple.com/verifyReceipt';
else
$store = 'https://buy.itunes.apple.com/verifyReceipt';
$postData = json_encode(array('receipt-data' => $receipt));
$curlHandle = curl_init($store);
curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlHandle, CURLOPT_POST, true);
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postData);
$encodedResponse = curl_exec($curlHandle);
curl_close($curlHandle);
if (!$encodedResponse)
return result(ERROR_VERIFICATION_NO_RESPONSE, 'Payment could not be verified - no response data. This was sandbox? ' . ($sandbox ? 'YES' : 'NO'));
$response = json_decode($encodedResponse);
$status = $response->{'status'};
$decodedReceipt = $response->{'receipt'};
if ($status)
return result(ERROR_VERIFICATION_FAILED, 'Payment could not be verified (status = ' . $status . ').');
logToFile(print_r($decodedReceipt, true));
$productID = $decodedReceipt->{'product_id'};
$transactionID = $decodedReceipt->{'transaction_id'};
$originalTransactionID = $decodedReceipt->{'original_transaction_id'};
if (!beginsWith($productID, PRODUCT_ID_PREFIX))
return result(ERROR_INVALID_PRODUCT_ID, 'Invalid Product Identifier');
$db = Database::get();
$statement = $db->prepare('SELECT * FROM transactions WHERE transaction_id = ?');
$statement->bindParam(1, $transactionID, PDO::PARAM_STR, 32);
$statement->execute();
if ($statement->rowCount())
{
logToFile("Already processed $transactionID.");
return result(ERROR_TRANSACTION_ALREADY_PROCESSED, 'Already processed this transaction.');
}
else
{
logToFile("Adding $transactionID.");
$statement = $db->prepare('INSERT INTO transactions(transaction_id, product_id, original_transaction_id) VALUES (?, ?, ?)');
$statement->bindParam(1, $transactionID, PDO::PARAM_STR, 32);
$statement->bindParam(2, $productID, PDO::PARAM_STR, 32);
$statement->bindParam(3, $originalTransactionID, PDO::PARAM_STR, 32);
$statement->execute();
}
return result(SUCCESS);
}
Actual executable PHP script:
$receipt = $_POST['receipt'];
$sandbox = $_POST['sandbox'];
$returnValue = validateReceipt($receipt, $sandbox);
header('content-type: application/json; charset=utf-8');
echo json_encode($returnValue);