I have created a class which handles the purchases on In-App Purchases and also the validating of receipts. A while ago I used to use the transactionReceipt property on an SKPaymentTransaction, but have updated my code a fair amount and now use appStoreReceiptURL on the [NSBundle mainBundle].
Basically it seems as though my receipt is being sent to Apple's server in an acceptable manner, but I keep getting the status code of 21002. In auto-renewable subscriptions I know that this means the receipt is not in an acceptable format, however I have no idea what this status means in regard to an in-app purchase receipt.
Here is the local method validating the receipt:
/**
* Validates the receipt.
*
* @param transaction The transaction triggering the validation of the receipt.
*/
- (void)validateReceiptForTransaction:(SKPaymentTransaction *)transaction
{
// get the product for the transaction
IAPProduct *product = self.internalProducts[transaction.payment.productIdentifier];
// get the receipt as a base64 encoded string
NSData *receiptData = [[NSData alloc] initWithContentsOfURL:[NSBundle mainBundle].appStoreReceiptURL];
NSString *receipt = [receiptData base64EncodedStringWithOptions:kNilOptions];
NSLog(@"Receipt: %@", receipt);
// determine the url for the receipt verification server
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)
{
// create a block to be called whenever a filue is hit
void (^failureBlock)(NSString *failureMessage) = ^void(NSString *failureMessage)
{
[[NSOperationQueue mainQueue] addOperationWithBlock:
^{
// log the failure message
NSLog(@"%@", failureMessage);
// if we have aready tried refreshing the receipt then we close the transaction to avoid loops
if (self.transactionToValidate)
product.purchaseInProgress = NO,
[[SKPaymentQueue defaultQueue] finishTransaction:transaction],
[self notifyStatus:@"Validation failed." forProduct:product],
self.transactionToValidate = nil;
// if we haven't tried yet, we'll refresh the receipt and then attempt a second validation
else
self.transactionToValidate = transaction,
[self refreshReceipt];
}];
};
// check for an error whilst contacting the server
if (connectionError)
{
failureBlock([[NSString alloc] initWithFormat:@"Failure connecting to server: %@", connectionError]);
return;
}
// cast the response appropriately
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
// parse the JSON
NSError *jsonError;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
// if the data did not parse correctly we fail out
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;
}
// if the JSON was successfully parsed pull out status code to check for verification success
NSInteger statusCode = [json[@"status"] integerValue];
NSString *errorDescription = json[@"error"];
// if the verification did not succeed we fail out
if (statusCode != 0)
{
NSString *failureMessage = [[NSString alloc] initWithFormat:@"Failure verifying receipt: %@", errorDescription];
failureBlock(failureMessage);
}
// otherwise we have succeded, yay
else
NSLog(@"Successfully verified receipt."),
[self provideContentForCompletedTransaction:transaction productIdentifier:transaction.payment.productIdentifier];
}];
}
The important PHP function on the server does this:
/**
* Validates a given receipt and returns the result.
*
* @param receipt Base64-encoded receipt.
* @param sandbox Boolean indicating whether to use sandbox servers or production servers.
*
* @return Whether the reciept is valid or not.
*/
function validateReceipt($receipt, $sandbox)
{
// determine url for store based on if this is production or development
if ($sandbox)
$store = 'https://sandbox.itunes.apple.com/verifyReceipt';
else
$store = 'https://buy.itunes.apple.com/verifyReceipt';
// set up json-encoded dictionary with receipt data for apple receipt validator
$postData = json_encode(array('receipt-data' => $receipt));
// use curl library to perform web request
$curlHandle = curl_init($store);
// we want results returned as string, the request to be a post, and the json data to be in the post fields
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 we received no response we return the error
if (!$encodedResponse)
return result(ERROR_VERIFICATION_NO_RESPONSE, 'Payment could not be verified - no response data. This was sandbox? ' . ($sandbox ? 'YES' : 'NO'));
// decode json response and get the data
$response = json_decode($encodedResponse);
$status = $response->{'status'};
$decodedReceipt = $response->{'receipt'};
// if status code is not 0 there was an error validation receipt
if ($status)
return result(ERROR_VERIFICATION_FAILED, 'Payment could not be verified (status = ' . $status . ').');
// log the returned receipt from validator
logToFile(print_r($decodedReceipt, true));
// pull out product id, transaction id and original transaction id from infro trurned by apple
$productID = $decodedReceipt->{'product_id'};
$transactionID = $decodedReceipt->{'transaction_id'};
$originalTransactionID = $decodedReceipt->{'original_transaction_id'};
// make sure product id has expected prefix or we bail
if (!beginsWith($productID, PRODUCT_ID_PREFIX))
return result(ERROR_INVALID_PRODUCT_ID, 'Invalid Product Identifier');
// get any existing record of this transaction id from our database
$db = Database::get();
$statement = $db->prepare('SELECT * FROM transactions WHERE transaction_id = ?');
$statement->bindParam(1, $transactionID, PDO::PARAM_STR, 32);
$statement->execute();
// if we have handled this transaction before return a failure
if ($statement->rowCount())
{
logToFile("Already processed $transactionID.");
return result(ERROR_TRANSACTION_ALREADY_PROCESSED, 'Already processed this transaction.');
}
// otherwise we insert this new transaction into the database
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);
}
The actual PHP script being executed is:
$receipt = $_POST['receipt'];
$sandbox = $_POST['sandbox'];
$returnValue = validateReceipt($receipt, $sandbox);
header('content-type: application/json; charset=utf-8');
echo json_encode($returnValue);
Comparing your PHP with mine (which I know works) is difficult because I am using HTTPRequest rather than the raw curl APIs. However, it seems to me that you are setting the "{receipt-data:..}" JSON string as merely a field in the POST data rather than as the raw POST data itself, which is what my code is doing.
curl_setopt($curlHandle, CURLOPT_POST, true);
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postData); // Possible problem
$encodedResponse = curl_exec($curlHandle);
Compared to:
$postData = '{"receipt-data" : "'.$receipt.'"}'; // yay one-off JSON serialization!
$request = new HTTPRequest('https://sandbox.itunes.apple.com/verifyReceipt', HTTP_METH_POST);
$request->setBody($postData); // Relevant difference...
$request->send();
$encodedResponse = $request->getResponseBody();
I have changed my variable names a bit to make them match up with your example.
the code 21002 means "The data in the receipt-data property was malformed or missing."
you can find it in https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html
below code is my class for appstore in-app verifyRecepip, GuzzleHttp is required, you can install it by composer require guzzlehttp/guzzle
https://github.com/guzzle/guzzle
<?php
namespace App\Libraries;
class AppStoreIAP
{
const SANDBOX_URL = 'https://sandbox.itunes.apple.com/verifyReceipt';
const PRODUCTION_URL = 'https://buy.itunes.apple.com/verifyReceipt';
protected $receipt = null;
protected $receiptData = null;
protected $endpoint = 'production';
public function __construct($receipt, $endpoint = self::PRODUCTION_URL)
{
$this->receipt = json_encode(['receipt-data' => $receipt]);
$this->endpoint = $endpoint;
}
public function setEndPoint($endpoint)
{
$this->endpoint = $endpoint;
}
public function getReceipt()
{
return $this->receipt;
}
public function getReceiptData()
{
return $this->receiptData;
}
public function getEndpoint()
{
return $this->endpoint;
}
public function validate($bundle_id, $transaction_id, $product_code)
{
$http = new \GuzzleHttp\Client([
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
],
'timeout' => 4.0,
]);
$res = $http->request('POST', $this->endpoint, ['body' => $this->receipt]);
$receiptData = json_decode((string) $res->getBody(), true);
$this->receiptData = $receiptData;
switch ($receiptData['status']) {
case 0: // verify Ok
// check bundle_id
if (!empty($receiptData['receipt']['bundle_id'])) {
$receipt_bundle_id = $receiptData['receipt']['bundle_id'];
if ($receipt_bundle_id != $bundle_id) {
throw new \Exception('bundle_id not matched!');
}
}
// check transaction_id , product_id
if (!empty($receiptData['receipt']['in_app'])) {
$in_app = array_combine(array_column($receiptData['receipt']['in_app'], 'transaction_id'), $receiptData['receipt']['in_app']);
if (empty($in_app[$transaction_id])) {
throw new \Exception('transaction_id is empty!');
}
$data = $in_app[$transaction_id];
if ($data['product_id'] != $product_code) {
throw new \Exception('product_id not matched!');
}
} else {
$receipt_transaction_id = $receiptData['receipt']['transaction_id'];
$receipt_product_id = $receiptData['receipt']['product_id'];
if ($receipt_transaction_id != $transaction_id || $product_id != $product_code) {
throw new \Exception('tranaction_id not matched!');
}
}
break;
case 21007:// sandbox order validate in production will return 21007
if ($this->getEndpoint() != self::SANDBOX_URL) {
$this->setEndPoint(self::SANDBOX_URL);
$this->validate($bundle_id, $transaction_id, $product_code);
} else {
throw new \Exception('appstore error!');
}
break;
default:
throw new \Exception("[{$receiptData['status']}]appstore error!");
break;
}
return $receiptData;
}
}
I think Morteza M is correct. I did a test and got reply(JSON) like:
{
'status':
'environment': 'Sandbox'
'receipt':
{
'download_id':
....
'in_app":
{
'product_id':
....
}
....
}
}
来源:https://stackoverflow.com/questions/19785803/validating-appreceiptstoreurl-returning-21002-status