【问题标题】:Validating receipt for iOS in-app purchase always returns error 21002验证 iOS 应用内购买的收据总是返回错误 21002
【发布时间】:2021-02-18 02:28:41
【问题描述】:

我正在服务器端验证我的消耗品应用内购买。

也就是说,我通过以下方式从客户端获取收据:

    .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)") 打印以下内容:

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

然后将回执字符串发送到服务器:

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

如您所见,上面的代码尝试生产 verifyReceipt 端点,如果失败并出现沙盒错误 (21007),它会尝试沙盒端点。但是它从不尝试沙盒端点,因为第一次尝试会出现不同的错误:

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

我不知道为什么会发生此错误。如果这有什么不同,我正在沙盒中进行测试。

知道为什么我不断收到此错误吗?

编辑:我在 3 天内不断测试,尝试了所有方法,但每次仍然收到 21002 相同的错误。我很迷茫。

【问题讨论】:

  • 您是否检查了收据字符串在您的服务器上的外观?由于编码或字符转义,它可能格式错误。此外,我相对确定您无法验证消耗品,因为它们没有保留在收据中。
  • 是的,receiptString 在服务器上看起来与我在客户端设备上打印时完全相同。关于您的其他观点,我不知道receiptString 包含字符转义或无法验证消耗性收据。必须查一下。
  • 这能回答你的问题吗? IOS receipt validation error 21002
  • 嗯不是真的 - 正如您在上面的收据字符串中看到的那样,没有字符转义。
  • @PaulSchröder 您可以验证消耗品。只要它们在支付队列中(交易未完成),它们就会被持久化。

标签: ios swiftui in-app-purchase storekit ios14


【解决方案1】:

看起来您正试图在模拟器中使用 storekit 本地测试环境验证收据(在 wwdc2020 上提出),对吧?我的意思是您以这种方式在应用程序中获得收据,无论您是通过应用程序的 api 调用还是某个单独的后端应用程序检查此收据(是的,我已检查)

如果是这样,它将不起作用

你应该在没有这个新功能的情况下做所有的事情,就像它在 13 和更低版本中一样(通过在 appstoreconnect 中创建产品等),这样收据验证就可以正常工作。

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

【讨论】:

  • 感谢您的回复。我已经在 App Store Connect 中创建了产品:i.imgur.com/9AuEXy1.png - 我如何确保我的应用从那里获取 productID 而不是我的沙箱 Configuration.storekit?:i.imgur.com/nd2lFns.png - 我使用相同的 productID在我的代码中:i.imgur.com/xIAjMNS.png 就像我在 App Store Connect 和 Configuration.storekit 中所做的那样
  • developer.apple.com/documentation/xcode/… 查看禁用 storekit 测试部分
  • 是的,这就是问题所在 - 在 App Store Connect 中使用 Configuration.storekit 而不是真正的 productId。在Product > Scheme > Edit Scheme 中禁用Configuration.storekit 后 - 它现在可以工作了!非常感谢@yablitsev 和@ARR
  • 我开始使用本地配置文件进行测试,但随后将其删除,我仍然收到21002。我通过更改价格验证了我依赖 App Store 信息,并且我看到来自产品请求的新价格。我的应用仍处于“准备提交”阶段,因为它是第一次发布,并且应用内购买处于“准备提交”状态。沙盒测试应该在这种状态下工作还是我必须先提交应用程序?
【解决方案2】:

首先,您的收据肯定格式不正确,21002 状态码表示它格式不正确。你也可以在这里查看,https://www.revenuecat.com/apple-receipt-checker

您的 swift 和 js 代码似乎 100% 合法,所以不用担心!

问题可能是您的收据文件已损坏,您能否将应用程序从您的设备中完全删除并重新安装?

或者在不同的设备上试试。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-26
    • 1970-01-01
    • 2014-07-19
    • 2011-10-12
    • 2010-11-20
    • 1970-01-01
    相关资源
    最近更新 更多