且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

验证iOS应用内购买收据始终返回错误21002

更新时间:2022-12-31 09:57:12

看起来像您正在尝试在模拟器(建议在wwdc2020上)中使用storekit本地测试环境来验证收货,对吗?我的意思是您以这种方式在应用程序中获取收据,无论您是通过应用程序还是某些单独的后端应用程序(是的,我被选中)中的api调用来检查此收据都没关系

如果是这样,它将无法正常工作

您应该在没有此新功能的情况下做所有事情,例如在13及以下版本中(通过在appstoreconnect中创建产品等),这样收据验证就可以正常工作.

p.s.我在本地模拟器中测试应用内购买时遇到了同样的问题

I am validating my consumable in-app purchase on the server-side.

That is, I get the receipt from the client-side via:

    .onChange(of: self.storeObserver.paymentStatus) { status in
        switch status {
        case .purchasing:
            print("Payment status: purchasing")
        case .failed:
            self.creatingGame = false
            print("Payment status: failed")
        case .deferred:
            print("Payment status: deferred")
        case .restored:
            print("Payment status: restored")
        case .purchased:
            // Get the receipt if it's available
            if Bundle.main.appStoreReceiptURL == nil {
                print("appStoreReceiptURL is nil")
            }
            if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
                FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
                do {
                    let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)

                    let receiptString = receiptData.base64EncodedString(options: [])
                    print("receiptString: \(receiptString)")
                    // Read receiptData
                    createGame(receiptString: receiptString)
                }
                catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
            }
            
            print("Payment status: purchased")
        default:
            print("Payment status: default")
        }
    }

private func createGame(receiptString: String){
    let data: [String:Any?] = [
        "gameName": self.gameName,
        "receipt": receiptString
    ]
    callFunction(name: "validateReceipt", data: data){ result, err in
    }

print("receiptString: (receiptString)") prints the following:

receiptString: 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

And then the receipt string is sent to the server:

exports.validateReceipt = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('permission-denied', 'The function must be called while authenticated.');
  }
  if (!data.receipt) {
    throw new functions.https.HttpsError('permission-denied', 'receipt is required');
  }
  // Now we fetch the receipt from Apple
  let body = {
    'receipt-data': data.receipt,
    // 'password': 'MY_SECRET_PASSWORD', // Not needed for Consumable IAP's
    'exclude-old-transactions': true
  };
  const options = {
    method: 'post',
    body: JSON.stringify(body),
    headers: {'Content-Type': 'application/json'},
  };
  return validateReceiptData('https://buy.itunes.apple.com/verifyReceipt', options, data, context);
});

function validateReceiptData(url, options, data, context) {
    var retries = 0

  return fetch(url, options).then(result => {
    return result.json();
      }).then(data => {
        if (data.status === 21007 && retries === 0) {
            retries += 1
          // Retry with sandbox URL
          console.log("Try sandbox URL");
          return validateReceiptData('https://sandbox.itunes.apple.com/verifyReceipt', options, data, context);
        }
        console.log(`data.status: ${data.status}`); // prints status code 21002
        // Process the result
        if (data.status !== 0) {
            console.log("The status code is not 0, so the receipt is invalid"); // function returns here
          return false;
        }
        const latestReceiptInfo = data.latest_receipt_info[0];
        console.log(`Receipt data is valid: ${latestReceiptInfo}`);
        if (data.type === "join"){
            return joinGame(data, context)
        } 
        else if (data.type === "create"){
            return createGame(data, context)
        }
        return 400;
      });
}

As you can see, the above code tries the production verifyReceipt endpoint, and if that fails with a sandbox error (21007), it tries the sandbox endpoint. However it never tries the sandbox endpoint as a different error comes up the first try:

21002
The data in the receipt-data property was malformed or the service experienced a temporary issue. Try again.

I have no idea why this error occurs. I am testing in sandbox if that makes any difference.

Any idea why I keep getting this error?

Edit: I have been getting the same error for 3 days with constant testing, trying everything and still getting a 21002 every time. I'm quite lost.

looks like you trying to verify receipt with storekit local testing environment in simulator (proposed on wwdc2020), right? i mean you getting receipt in application this way, doesn't matter if you will check this receipt with api call from your application or the some separate backend application (yep, i'm checked)

if so, it will not work

you should do all the things without this new feature, as it was on 13 and below (by creating products in appstoreconnect and so on), this way receipt verification works as it should.

p.s. i faced the same problem with testing in-app purchases in the simulator locally