1

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.

Zorgan
  • 8,227
  • 23
  • 106
  • 207
  • Did you check how the receipt string looks on your server? It could be malformed because of encoding or character escaping. Also, I am relatively sure you can't validate consumables because they are not persisted within the receipt. – Paul Schröder Feb 18 '21 at 10:17
  • Yes the `receiptString` looks the exact same on the server, to when I print it on the client device. Regarding your other points, I wasn't aware the receiptString contained character escaping or that consumable receipts can't be validated. Will have to look those up. – Zorgan Feb 18 '21 at 10:37
  • Does this answer your question? [IOS receipt validation error 21002](https://stackoverflow.com/questions/32836058/ios-receipt-validation-error-21002) – lorem ipsum Feb 19 '21 at 13:47
  • Hmm not really - as you can see in the receipt string above, there is no character escaping. – Zorgan Feb 19 '21 at 21:51
  • @PaulSchröder You can validate consumables. They are persisted as long as they are in the payment queue (transaction is not finished). – ARR Feb 23 '21 at 01:37
  • @Zorgan have you compared the complete receipt strings (char by char) from your local print and what your server receives? The receipt string contains special characters (for example /), and those get usually escaped when sending them via HTTP(s) – Paul Schröder Feb 23 '21 at 10:36
  • @PaulSchröder yes I can confirm it is the exact same string - my server contains the backslashes such as `/5` and `/7` as well – Zorgan Feb 23 '21 at 10:42

2 Answers2

7

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

zyablitsev
  • 371
  • 1
  • 3
  • Thanks for the response. I have already created the product in App Store Connect: https://i.imgur.com/9AuEXy1.png - how do I ensure that my app is taking the `productID` from there and not my sandbox `Configuration.storekit`?: https://i.imgur.com/nd2lFns.png - I use the same `productID` in my code: https://i.imgur.com/xIAjMNS.png as I do in both App Store Connect and `Configuration.storekit` – Zorgan Feb 23 '21 at 07:40
  • https://developer.apple.com/documentation/xcode/setting_up_storekit_testing_in_xcode look in the section disable storekit testing – ARR Feb 23 '21 at 10:12
  • Yes that was the problem - using `Configuration.storekit` and not the real `productId` in App Store Connect. After disabling `Configuration.storekit` in `Product > Scheme > Edit Scheme` - it now works! Thank you so much @yablitsev and @ARR – Zorgan Feb 23 '21 at 12:29
  • I started testing using the local config file but then deleted it and I'm still getting `21002`. I verified that I'm relying on the App Store info by changing the price and I see the new prices coming from the products request. My app is still in "Prepare for Submission" stage because it's the first launch, and the in-app purchases are in "Ready to Submit" state. Is Sandbox testing supposed to work in this state or do I have to submit the app first? – mota Mar 30 '21 at 07:05
3

First of all your receipt is definitely malformed, the 21002 status code means it is malformed. You can also check it here, https://www.revenuecat.com/apple-receipt-checker.

Your swift and js code seems to be 100% legit, so no worries about that!

What could possibly be the problem is that your receipt file is corrupt, could you please remove the app completely from your device and reinstall it?

Or try it on a different device.

ARR
  • 2,074
  • 1
  • 19
  • 28