【问题标题】:Validating appReceiptStoreURL Returning 21002 Status验证 appReceiptStoreURL 返回 21002 状态
【发布时间】:2013-11-16 03:20:03
【问题描述】:

我创建了一个类来处理应用内购买以及收据的验证。不久前,我曾经在 SKPaymentTransaction 上使用 transactionReceipt 属性,但我已经更新了相当数量的代码,现在在 [NSBundle mainBundle] 上使用 appStoreReceiptURL。

基本上,我的收据似乎以可接受的方式发送到 Apple 的服务器,但我不断收到 21002 的状态代码。在自动续订订阅中,我知道这意味着收据的格式不可接受,但是我不知道这种状态对于应用内购买收据意味着什么。

这是验证收据的本地方法:

/**
 *  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];

    }];
}

服务器上重要的 PHP 函数是这样做的:

    /**
     *  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);
    }

实际执行的 PHP 脚本是:

    $receipt            = $_POST['receipt'];
    $sandbox            = $_POST['sandbox'];
    $returnValue        = validateReceipt($receipt, $sandbox);

    header('content-type: application/json; charset=utf-8');

    echo json_encode($returnValue);

【问题讨论】:

  • 现在您的$response->{'receipt'} 对象不包括product_id 等。有一个inapp 数组您应该对其进行迭代。它包含用户进行的所有应用内购买。如果您在此数组中找到 product_id,则验证成功。否则无效。

标签: php ios objective-c receipt


【解决方案1】:

比较你的 PHP 和我的(我知道这可行)是很困难的,因为我使用的是 HTTPRequest 而不是原始的 curl API。但是,在我看来,您将“{receipt-data:..}”JSON 字符串设置为 POST 数据中的一个 field 而不是原始 POST 数据本身,这就是我的代码在做。

curl_setopt($curlHandle, CURLOPT_POST, true);
curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postData); // Possible problem
$encodedResponse = curl_exec($curlHandle);

相比:

$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();

我已经稍微更改了我的变量名,以使它们与您的示例相匹配。

【讨论】:

    【解决方案2】:

    代码 21002 表示“receipt-data 属性中的数据格式错误或丢失。”

    你可以在 https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

    下面的代码是我的appstore应用内verifyRecepip类,需要GuzzleHttp,可以通过composer require guzzlehttp/guzzlehttps://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;
      }
    }
    

    【讨论】:

      【解决方案3】:

      我认为 Morteza M 是正确的。我做了一个测试,得到了回复(JSON),比如:

      {
      'status':
      'environment': 'Sandbox'
        'receipt':
        {
          'download_id':
          ....
          'in_app":
          {
            'product_id':
            ....
          }
          ....
        }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-01-10
        • 2023-02-09
        • 1970-01-01
        • 1970-01-01
        • 2015-12-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多