dooboolab-community / react-native-iap

In App Purchase module for React Native!
https://react-native-iap.dooboolab.com
MIT License
2.78k stars 634 forks source link

iOS Receipt Validation Returns Status 21002 #1968

Closed liyamahendra closed 1 year ago

liyamahendra commented 1 year ago

Please use the Discussion board if you want to get some help. Please use issues to report bugs.

Description

On app startup, I want to check if the auto-renewing subscription is still active or not. I'm using the below code:

const isSubscriptionActive = async () => {
        if (Platform.OS === 'ios') {
            const availablePurchases = await RNIap.getAvailablePurchases();
            const sortedAvailablePurchases = availablePurchases.sort(
                (a, b) => b.transactionDate - a.transactionDate
            );
            const latestAvailableReceipt = sortedAvailablePurchases[0].transactionReceipt;

            const isTestEnvironment = __DEV__;
            const decodedReceipt = await RNIap.validateReceiptIos(
                {
                    'receipt-data': btoa(latestAvailableReceipt),
                    password: 'MY_SHARED_SECRET',
                },
                isTestEnvironment
            );
            console.log("decodedReceipt: ", decodedReceipt);
            const { latest_receipt_info: latestReceiptInfo } = decodedReceipt;
            console.log("latestReceiptInfo: ", latestReceiptInfo);
            const isSubValid = !!latestReceiptInfo.find(receipt => {
                const expirationInMilliseconds = Number(receipt.expires_date_ms);
                const nowInMilliseconds = Date.now();
                return expirationInMilliseconds > nowInMilliseconds;
            });
            return isSubValid;
        }
    }

However, the decodedReceipt returns {"status": 21002}

Expected Behavior

Should return the Receipt Info

Screenshots

Environment:

To Reproduce Steps to reproduce the behavior:

  1. Make a call to RNIap.initConnection()
  2. Make a call to RNIap.getAvailablePurchases() as shown in isSubscriptionActive method
  3. You'll notice the following output in console:
    decodedReceipt:  {"status": 21002}
    latestReceiptInfo:  undefined

[Optional] Additional Context

I'm using TypeScript.

andresesfm commented 1 year ago

@liyamahendra : have you tried it without reencoding with btoa? the receipt is already returned in base64

andresesfm commented 1 year ago

Closing in favor of: https://github.com/dooboolab/react-native-iap/issues/1967

liyamahendra commented 1 year ago

@andresesfm my initial attempt was without reencoding with btoa and it didn't work. Out of curiosity, I tried to encode it using base64 but no luck with that either. Sorry, I forgot to remove that from the issue details I posted.

I upgraded to 10.1.1 and still facing the same issue.

Any advise please? Do you need any specific details?

andresesfm commented 1 year ago

@liyamahendra in that case, I think it's related to this: https://stackoverflow.com/questions/32836058/ios-receipt-validation-error-21002.

liyamahendra commented 1 year ago

@andresesfm thank you for reopening the issue! I'm not doing server-side validation of the Receipt. I think the RNIap.validateReceiptIos call does validation against AppStore, right?

liyamahendra commented 1 year ago

One of the Stackoverflow answer recommends replacing \r and \n characters. Do you think that's required? I've done Receipt validation in native iOS code - hadn't faced such issue there.

andresesfm commented 1 year ago

Fixed here following the suggestion from SO: https://github.com/dooboolab/react-native-iap/pull/1977

andresesfm commented 1 year ago

Released as part of 10.1.2. Please consider making a contribution to the project

liyamahendra commented 1 year ago

Hi @andresesfm thank you for looking into this again. I installed the module from cloning your fork and checking the base_64_encoding_options branch.

Test the app but it still returns {"status": 21002}.

andresesfm commented 1 year ago

Have you tried looking at the receipt and see if there's something strange we are not seeing?

liyamahendra commented 1 year ago

I noticed that you merged the base_64_encoding_options. I installed 10.1.2 and made sure to test the app using TestFlight build.

This seeing the same issue.

I'll comment about the receipt shortly.

liyamahendra commented 1 year ago

I used the tool available here to decode the receipt using my shared secret. Following is the decoded receipt content:

{
  "environment": "Sandbox",
  "receipt": {
    "receipt_type": "ProductionSandbox",
    "adam_id": 0,
    "app_item_id": 0,
    "bundle_id": "my-bundle-id",
    "application_version": "13",
    "download_id": 0,
    "version_external_identifier": 0,
    "receipt_creation_date": "2022-09-20 17:42:53 Etc/GMT",
    "receipt_creation_date_ms": "1663695773000",
    "receipt_creation_date_pst": "2022-09-20 10:42:53 America/Los_Angeles",
    "request_date": "2022-09-20 17:45:44 Etc/GMT",
    "request_date_ms": "1663695944519",
    "request_date_pst": "2022-09-20 10:45:44 America/Los_Angeles",
    "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
    "original_purchase_date_ms": "1375340400000",
    "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
    "original_application_version": "1.0",
    "in_app": [
      {
        "quantity": "1",
        "product_id": "subscription.monthly",
        "transaction_id": "2000000157180412",
        "original_transaction_id": "2000000156809195",
        "purchase_date": "2022-09-16 18:26:40 Etc/GMT",
        "purchase_date_ms": "1663352800000",
        "purchase_date_pst": "2022-09-16 11:26:40 America/Los_Angeles",
        "original_purchase_date": "2022-09-16 18:21:41 Etc/GMT",
        "original_purchase_date_ms": "1663352501000",
        "original_purchase_date_pst": "2022-09-16 11:21:41 America/Los_Angeles",
        "expires_date": "2022-09-16 18:31:40 Etc/GMT",
        "expires_date_ms": "1663353100000",
        "expires_date_pst": "2022-09-16 11:31:40 America/Los_Angeles",
        "web_order_line_item_id": "2000000011165376",
        "is_trial_period": "false",
        "is_in_intro_offer_period": "false",
        "in_app_ownership_type": "PURCHASED"
      },
      // many such objects
    ]
  },
  "latest_receipt_info": [
    {
      "quantity": "1",
      "product_id": "subscription.monthly",
      "transaction_id": "2000000159150610",
      "original_transaction_id": "2000000156809195",
      "purchase_date": "2022-09-20 16:27:15 Etc/GMT",
      "purchase_date_ms": "1663691235000",
      "purchase_date_pst": "2022-09-20 09:27:15 America/Los_Angeles",
      "original_purchase_date": "2022-09-16 18:21:41 Etc/GMT",
      "original_purchase_date_ms": "1663352501000",
      "original_purchase_date_pst": "2022-09-16 11:21:41 America/Los_Angeles",
      "expires_date": "2022-09-20 16:32:15 Etc/GMT",
      "expires_date_ms": "1663691535000",
      "expires_date_pst": "2022-09-20 09:32:15 America/Los_Angeles",
      "web_order_line_item_id": "2000000011359346",
      "is_trial_period": "false",
      "is_in_intro_offer_period": "false",
      "in_app_ownership_type": "PURCHASED",
      "subscription_group_identifier": "21017206"
    },
    // many such objects
  ],
  "latest_receipt": "MIKtXAY...=",
  "pending_renewal_info": [
    {
      "expiration_intent": "1",
      "auto_renew_product_id": "subscription.monthly",
      "is_in_billing_retry_period": "0",
      "product_id": "subscription.monthly",
      "original_transaction_id": "2000000156809195",
      "auto_renew_status": "0"
    }
  ],
  "status": 0
}
andresesfm commented 1 year ago

It looks like a valid JSON to me. So what's left is your SECRET, could it be that you are using the wrong one?

liyamahendra commented 1 year ago

Well, I used the same secret to decode the receipt in the tool above - so I don't believe its the wrong one.

andresesfm commented 1 year ago

In that case, try sending it manually with curl or fetch. If you notice, all that method does is post to an end point

liyamahendra commented 1 year ago

The request succeeds using CURL.

andresesfm commented 1 year ago

Can you please post here an example of the curl? Thank you for all your help

liyamahendra commented 1 year ago

Here is the complete curl command:

curl -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d '{"receipt-data":"MIKtYAYJKoZIhvcNAQcCoIKt.....", "password": "67a46f..."}' https://sandbox.itunes.apple.com/verifyReceipt
andresesfm commented 1 year ago

2 things come to mind: 1) You are using the URL that would be used if isTestEnvironment is passed as true. Can you verify that's the case? and 2) If that's still the case, you'd need to capture the request using your dev tools or proxy. Please let me know how that goes

liyamahendra commented 1 year ago

I verified the URL using Charles - the sandbox url is used. Please see the screencast below:

https://user-images.githubusercontent.com/911879/192083142-658459b4-1032-4ad1-8523-1ec3c6c8f036.mp4

liyamahendra commented 1 year ago

@andresesfm is there anything I can do to help you debug this issue and hopefully fix this?

How do you test & verify it on your end?

andresesfm commented 1 year ago

@liyamahendra can you please confirm that the request to sandbox captured on Charles is the same as the one you used in the cUrl?

liyamahendra commented 1 year ago

@andresesfm yes, I can confirm the requests were the same.

andresesfm commented 1 year ago

Check the headers and the encoding please

liyamahendra commented 1 year ago

@andresesfm below is the screenshot showing the headers ( and the URL as well)

image
andresesfm commented 1 year ago

@liyamahendra those are the response headers, I meant the request headers

alihassan143 commented 1 year ago

@andresesfm check this link Apple Developer Documentation that is not even an issue of the plugin developer have to apply simple checks and he is good to go just read this documentation learn about different status code based on receipt i think this issue need to be closed

liyamahendra commented 1 year ago

@alihassan143 sorry, not sure what checks you're referring to!

alihassan143 commented 1 year ago

@liyamahendra you can check apple documentation and based on different status code you can perform actions accordingly you have to do it in app

roots-ai commented 1 year ago

I tried this with 12.0.3 as well. Same 21002 error.

iambinodstha commented 1 year ago

currentPurchase return transactionReceipt and purchaseToken empty after subscription is successful. How can I validate receipt without transactionReceipt? Does anybody encounter same issue. If yes then how it is fixed?

{"originalTransactionDateIOS": "1666159614000", "originalTransactionIdentifierIOS": "2000000180630438", "productId": "com.quiptgsm.pkg3", "purchaseToken": "", "quantityIOS": 1, "transactionDate": 1666162562000, "transactionId": "2000000180630438", "transactionReceipt": ""}

alihassan143 commented 1 year ago

@iambinodstha are you using configuration.storekit

alihassan143 commented 1 year ago

@iambinodstha configuration.storekit always return empty you have to test it on real device

iambinodstha commented 1 year ago

@alihassan143 Yes I do have setup({storekitMode: 'STOREKIT2_MODE'}); configuration in my index file and also I am using real device but still getting empty receipt.

alihassan143 commented 1 year ago

@iambinodstha when testing on real remove storekit configuration file from your build and then test that if you use storekit configurations file its always return empty mostly the reciept validation should be managed on server side and apple also recommend that never validate that kind of information inn mobile

andresesfm commented 1 year ago

Storekit 2 does not have receipt validation, It's done via the JWT Token

alihassan143 commented 1 year ago

@andresesfm yes receipt validation should be manage on backend if developer don't have backend than he can validate receipt using apple rest api and can perform certain operations that apple return and clearly mentioned in the rest api documentation

iambinodstha commented 1 year ago

@alihassan143 @andresesfm thanks for the info. I have handled receipt validation in my backend. but the problem now is receipt expires within few hours after I subscribe to the 1 month package.

Here is the JavaScript Code to check Receipt is Expired or not:

const data = {
    "receipt-data": transactionReceipt,
    "password": password,
    "exclude-old-transaction": true
}

let url = isTest ? "https://sandbox.itunes.apple.com/verifyReceipt" : "https://buy.itunes.apple.com/verifyReceipt"
const result = await axios.post(
    url,
    data
)

const receiptData = result["data"]["latest_receipt_info"][0];
const expiry = receiptData.expires_date_ms;
const expired = Date.now() > expiry;

return {
    isExpired: expired
}

If I used enabled storekit 2, how can I validate subscription without purchase token and receipt?

alihassan143 commented 1 year ago

@iambinodstha you cannot validate receipt without token actually token is base64 string that contains all information related to user package in test the package is only for 5 minutes after that it cycle restarts again

liyamahendra commented 1 year ago

Just leaving a small update - this wasn't resolved for me. I ended by writing a react bridge for iOS, specifically to check the subscription status, and solved the issue.

AF-Hub commented 1 year ago

@alihassan143 sorry, not sure what checks you're referring to!

Hello @liyamahendra I got the same issue. Have you been able to sort it out since ?

liyamahendra commented 1 year ago

@AF-Hub please see the update I left above.

andresesfm commented 1 year ago

@liyamahendra would you care to share that code/ create a PR with your change? it could benefit the project a lot since you mention it is still an issue

liyamahendra commented 1 year ago

@andresesfm I don't think creating a PR would be a good choice because I used RMStore for this purpose.

liyamahendra commented 1 year ago

@andresesfm since I've native iOS experience, would you like me to debug & evaluate why this bridge is failing?

alihassan143 commented 1 year ago

@andresesfm can you provide more details did you are successfully be able to subscribe or purchase item through in app purchase if yes than validation thing is not package things actually you have to accept that validation thing is not package does the only just call the apple api and apple servers responed according to your data validations have to be manage by the developers

andresesfm commented 1 year ago

@andresesfm since I've native iOS experience, would you like me to debug & evaluation why this bridge is failing?

I just pushed a fix, please let us know if any issues

atkristin commented 1 year ago

In order to validate a receipt, it must be purchased through a build in TestFlight.

Any receipts purchased with a build outside TestFlight return a 21002 (either real device or simulator)

liyamahendra commented 1 year ago

@atkristin thanks for the information. I can confirm that I was testing the InApp Purchase through TestFlight build.

jimzou commented 1 year ago

If you using local StoreKit config that the receipt is not valid because the certificate is created on your computer so the both sandbox and prod doesn't accept the receipt. It is recommended to verify through the server, refer to https://tkzo.jp/blog/flutter-iap-implementation/