React-native-iap: iOS Receipt Validation from Server Side

Created on 28 Jan 2020  ·  9Comments  ·  Source: dooboolab/react-native-iap

I am using my backend server to validate the iOS receipt. For that all I've done is passed the purchase.transactionReceipt to the application server in a post request. I want to know if purchase.transactionReceipt returns a base64 encoded receipt data or what? I'm getting error: 21002 with the current implementation

requestServerForReceiptVerification = (purchase: InAppPurchase | SubscriptionPurchase) => {
        const receipt = purchase.transactionReceipt;
        if (receipt) {
            // Hit Server API for Receipt Validation
            if (Platform.OS == 'ios') {
                let headers = {
                    'Content-Type': 'application/json'
                }
                Stores.UserStore.hitVerifyiOSReceiptAPI(receipt, headers, this.dropdown, (response: any) => {
                    finishTransaction(purchase).then(() => {
                        console.warn('Trasaction Finished');
                    }).catch((error) => {
                        console.warn(error.message);
                    })
                })
            } 
        }
    }
📱 iOS 🕵️‍♂️ need more investigation 🚶🏻 stale

Most helpful comment

check this one

const IOS_SHARED_PWD = "********";

async function validateIOS() {
    const latestPurchase = await getLatestPurchase();
    if (latestPurchase) {
        return false;
    }

    return RNIap.validateReceiptIos(
        {
            "receipt-data": latestPurchase.transactionReceipt,
            "password": IOS_SHARED_PWD,
            "exclude-old-transactions": false
        },
        false
    )
        .then(uncheckedValidation => {
            //check test receipt
            if (uncheckedValidation?.status === 21007) {
                return RNIap.validateReceiptIos(
                    {
                        "receipt-data": latestPurchase.transactionReceipt,
                        "password": IOS_SHARED_PWD,
                        "exclude-old-transactions": false
                    },
                    true
                );
            } else {
                return uncheckedValidation;
            }
        })
        .then(checkedValidation => {
            return isValidReceipt(checkedValidation);
        });
}

function getLatestPurchase() {
    return RNIap.getAvailablePurchases().then(purchases => {
        return purchases?.sort((a, b) => Number(b.transactionDate) - Number(a.transactionDate))?.[0] || null;
    });
}

function isValidReceipt(checkedValidation) {
    if (!checkedValidation) {
        return false;
    }

    // check is valid validation request
    if (checkedValidation.status === 21006 || checkedValidation.status === 21010) {
        return false;
    }

    const { latest_receipt_info: latestReceiptInfo } = checkedValidation;
    const latestReceipt = latestReceiptInfo
        ?.sort((a, b) => Number(b.purchase_date_ms) - Number(a.purchase_date_ms))
        ?.find(receipt => receipt.product_id === "some.product.id")?.[0];

    // no receipt
    if (!latestReceipt) {
        return false;
    }

    // refunded receipt
    if (latestReceipt.cancellation_date) {
        return false;
    }

    // expired receipt
    if (Number(latestReceipt.expires_date_ms) < Date.now()) {
        return false;
    }

    return checkedValidation.status === 0;
}

All 9 comments

@hyochan Can you help here please. The transaction receipt that I'm geting.. is it a base 64 encoded string? If not how can I convert it. I'm sending this to my BE server from where I'm hitting apple receipt validation API.

I'm looking into this same issue right now. On ios the transactionReceipt "looks" like a base64 string, but it's not. If I decode it on the device and output into the console, I get screens of garbage with occasional random words.

I then attempted to send it, as is up to my BE Server. There, I decoded it using dotnet core using this code:

string base64Decoded;
byte[] data = System.Convert.FromBase64String(receiptString);
base64Decoded = System.Text.Encoding.ASCII.GetString(data);
Console.WriteLine("ios receiptString:");
Console.WriteLine(base64Decoded);

The output for that is:
Screen Shot 2020-02-07 at 10 58 37 AM

My thought is that the receipt had each entry encoded...then the entire receipt encoded. I was hoping to find an answer here before I started testing that theory...but I'm going to go ahead and do that.

I've made a bit of progress. I was never able to get the validateReceiptIos function to return anything that could be decoded. My thought on that is there was a change in the way that Apple encodes it.

I was able to get usable information by passing the raw string up to my server and then posting it to the verifyReceipt endpoint. I used this as a guide: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

@leelandclay:
"I was able to get usable information by passing the raw string up to my server and then posting it to the verifyReceipt endpoint."
Did you just sent that transactionReceipt string to BE ?
I've went through the Apple Documentation but there the method of creating the receipt data is for the native iOS Application wherein we get appStoreReceiptURL.

Yes. Pass the object you got from the iap up to a server then send it to the website and you get back a JSON object.

OK...Here's where I'm at right now....

This is the code that I have within my app:

this.purchaseUpdateSubscription = purchaseUpdatedListener(
  (purchase: InAppPurchase | SubscriptionPurchase) => {
    console.log('purchaseUpdateSubscription called');
    if (purchase) {
      try {
        sendTransactionReceipt(this.state.profile.userId, purchase.transactionReceipt, this.state.token)
          .then(transactionReceipt => {
            finishTransaction(purchase, false)
              .then(finish => {
                this.setState({ transactionReceipt }, () => { this.sendingTransactionReceipt = false; });
              })
              .catch(err => {
                console.log('FinishTransaction ERROR: ' + err);
                this.sendingTransactionReceipt = false;
              });
          })
          .catch(() => {
            this.sendingTransactionReceipt = false;
          });
      } catch (ackErr) {
        console.warn('ackErr', ackErr);
      }
    }
  },
);

On the BE Server, I simply create the following object and POST it to the /verifyReceipt endpoint (using the server locations en the above documentation).

{
  "receipt-data": <purchase.transactionReceipt received from app>,
  "password": <secret password from within your apple account>,
  "exclude-old-transactions": true
}

I'm able to get responses back with valid information. Part of the response is a "latest_receipt" entry (used later). I store everything in my database as well as pass the information back down to the app so that it can call finishTransaction and store the valid receipt in state to be used.

I'm also working on a cron job that will execute once a day to grab all subscriptions that are showing as active and passed the expires date. That follows the same process as far as sending the information to the verifyReceipt. The "receipt-data" that I pass in is the "latest_receipt" that I stored from the previous verifyReceipt call.

Apparently, there's some weird way that the sandbox handles auto-renewing subscriptions, so I'm waiting to get a response on the Apple Developer Forums on what I should actually be looking for.

The situation that I'm seeing is that after purchasing a subscription, I get a response that shows auto renewing as true (pending_renewal_info[0].auto_renew_status from verifyReceipt shows as 1) and the expires_date entries (there's 3) show that it's in the future (5 minutes). If I wait until after the expires time and call verifyReceipt again, it will show the exact same values for the expires_date and auto renew.

My plan was to use the condition of auto renews === false and expires time < current time to determine whether to deactivate the subscription. Has anyone else seen this response from the verifyReceipt????

check this one

const IOS_SHARED_PWD = "********";

async function validateIOS() {
    const latestPurchase = await getLatestPurchase();
    if (latestPurchase) {
        return false;
    }

    return RNIap.validateReceiptIos(
        {
            "receipt-data": latestPurchase.transactionReceipt,
            "password": IOS_SHARED_PWD,
            "exclude-old-transactions": false
        },
        false
    )
        .then(uncheckedValidation => {
            //check test receipt
            if (uncheckedValidation?.status === 21007) {
                return RNIap.validateReceiptIos(
                    {
                        "receipt-data": latestPurchase.transactionReceipt,
                        "password": IOS_SHARED_PWD,
                        "exclude-old-transactions": false
                    },
                    true
                );
            } else {
                return uncheckedValidation;
            }
        })
        .then(checkedValidation => {
            return isValidReceipt(checkedValidation);
        });
}

function getLatestPurchase() {
    return RNIap.getAvailablePurchases().then(purchases => {
        return purchases?.sort((a, b) => Number(b.transactionDate) - Number(a.transactionDate))?.[0] || null;
    });
}

function isValidReceipt(checkedValidation) {
    if (!checkedValidation) {
        return false;
    }

    // check is valid validation request
    if (checkedValidation.status === 21006 || checkedValidation.status === 21010) {
        return false;
    }

    const { latest_receipt_info: latestReceiptInfo } = checkedValidation;
    const latestReceipt = latestReceiptInfo
        ?.sort((a, b) => Number(b.purchase_date_ms) - Number(a.purchase_date_ms))
        ?.find(receipt => receipt.product_id === "some.product.id")?.[0];

    // no receipt
    if (!latestReceipt) {
        return false;
    }

    // refunded receipt
    if (latestReceipt.cancellation_date) {
        return false;
    }

    // expired receipt
    if (Number(latestReceipt.expires_date_ms) < Date.now()) {
        return false;
    }

    return checkedValidation.status === 0;
}

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

@leelandclay Thanks for putting up your solutions. This really helped a lot and we finally were able to validate our receipt. Thanks !!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ramondelmondo picture ramondelmondo  ·  4Comments

MacMillan13 picture MacMillan13  ·  3Comments

sanilcgs picture sanilcgs  ·  3Comments

iutin picture iutin  ·  4Comments

bakedbean picture bakedbean  ·  5Comments