Validating iOS Subscription Receipts in React Native & Node.js

Managing transaction receipts and persisting them with user identifiers

Image for post
Image for post

Transaction receipts are a key element in managing subscriptions through in-app purchases, as well as for automatically renewing and cancelling subscriptions based on the receipt status and validity. They should be handled with care at the initial purchase stage, ensuring that they are persisted in your backend database and stored in a secure manner.

This article walks through the process of generating a transaction receipt in React Native, before sending it to your server with user identifiers in order to persist a transaction to a user account. Note that even if the reader is using native iOS APIs to manage in-app purchases within their app, that receipt can still be sent to the JavaScript side via an Express route and persisted in their backend database.

If the reader has not yet set up in-app purchases within their apps and would like to do so before following this piece, check out my article on setting up iOS subscriptions in React Native: React Native: Subscriptions with In-App Purchases. This piece will act as a natural follow-on to the setup discussed.

There are three key pieces to validating transaction receipts and persisting them with user identifiable metadata:

  • Sending transaction receipts to your server upon the initial in-app purchase of a subscription. Importantly, this initial purchase is also the only opportunity to associate a receipt to a user ID or unique account identifier: Apple do not allow ad-hoc data that could be tied into a user’s identity, and is therefore not supplied in their webhook service. Instead, we must associate this info when that initial receipt is generated. Receipts are also used later in the subscription lifecycle when it comes to renewing or cancelling, and should not be discarded under any circumstances.
  • Validating the receipt server-side. There is a security risk inherent with web services that could entail users spoofing transaction receipts in an attempt to obtain a free subscription to your app. This can be prevented by validating the receipt server-side before persisting it in a backend database. We will be using a package called node-apple-receipt-verify to carry out the validation, that provides a simple API for contacting Apple servers for the validation.
  • Persisting the receipt with account identifiers. A receipt should be linked to some user or account so your infrastructure knows which user or account initiated a transaction. This becomes very important when automatically renewing or cancelling a subscription with your own NodeJS runtime (that will be the subject of another article). This section will therefore document how to persist receipt and UIDs in a MongoDB collection on an Express server.

Let’s start with the central piece of data of an in-app purchase — the transaction receipt. Let’s first discuss how packages like react-native-iap generate the receipt upon an in-app purchase, and how to send a transaction receipt to your backend database.

Validating Receipts upon In-App Purchase

A transaction receipt is generated upon an in-app purchase (IAP) being made, and therefore are generated and sent directly to the device that initiated the IAP. These receipts are very important for multiple reasons:

  • The initial transaction receipt (the original receipt generated when purchasing a subscription) can be used for validation at any stage of the subscription cycle. Validating the initial receipt with Apple’s servers will return the current subscription status even if further payments have been made for the subscription in question.

The same can be said with receipts for payments further down the subscription lifecycle, whereby any receipt of a successful transaction can fetch the entire purchase history of that subscription.

  • This initial receipt is the only opportunity a developer has to link an unique account identifier to the receipt itself. Receipts are somewhat anonymous; they do not contain an Apple ID or anything to link the transaction to a particular user of your app — this is undoubtedly for privacy reasons.

These digital receipts really do live up to the receipt definition, in that they are proof of a particular purchase that can be used at any time to validate that purchase. Unlike a grocery receipt that we tend to immediately discard, developers are going to want to securely persist these digital receipts in their backend database.

The issue with point 2 lies with the fact that you must know which user account to update in the event a particular subscription is cancelled or updated to another tier. Subscription webhook data does include a receipt (the entire response body is documented here), but with no means to link that receipt to an account or user.

To resolve this issue, a simple solution is to send any unique identifiers (an user email address, unique object ID, etc) you require to get your subscription service working alongside the generated transaction receipt when the initial purchase is being made.

The initial purchase of a subscription acts as the only opportunity to link a receipt to a user. Intuitively, a user has to be signed in to their account, or authenticated in some way, in order to make an in-app purchase. Unique identifiers or authentication tokens will therefore be available within the app at the time of purchase.

For demonstration purposes, let’s turn our attention to when a transaction is generated with react-native-iap.

The APIs of react-native-iap have been visited in detail in the article mentioned above: React Native: Subscriptions with In-App Purchases.

The package conveniently supplies an event listener named purchaseUpdatedListener that is executed whenever a new in-app purchase is initiated (a subscription or one-time purchase). We typically wrap this event listener within useEffect, as to remove the listener when the containing component unmounts.

It is within the purchaseUpdatedListener that custom functions can be embedded for handling ad-hoc tasks for managing a new purchase — such as sending vital transaction info to your backend server.

Without delving into the react-native-iap workflow in detail (I have done so in my In-App Purchase article), note that the receipt of the in-app purchase is automatically generated and is supplied by the event listener. We are then free to handle the receipt as we please in the space labelled with Handle in-app purchase in the following example:

// responding to in-app purchases with `react-native-iap`useEffect(() => {
purchaseUpdateSubscription = purchaseUpdatedListener(
async (purchase) => {
const receipt = purchase.transactionReceipt;
if (receipt) {
try {
if (Platform.OS === 'ios') {
finishTransactionIOS(purchase.transactionId);
}
await finishTransaction(purchase);
/* Handle in-app purchase */
await processNewSubscription(purchase);
/*************************************/
} catch (ackErr) {
// process error
}
}
},
);
...
return (() => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
purchaseUpdateSubscription = null;
}
...
})
}, []);
const processNewSubscription = async (purchase) => {
...
}

It is in this labelled space that we can either embed more logic, or call other component functions (or even imported functions) to execute as an in-app purchase is being made. The above example calls the processNewSubscription method in this space, a separate component function defined outside of the event listener itself.

This space also gives you the opportunity to persist subscription data into your Redux store (such as the subscribed product), that will then trigger a re-render of the components that depend on that state. This should automatically update your UI to a “subscribed” state.

Because these listeners are wrapped in a containing component (that is subject to props and state) you are free to pass any data identifying the user directly into the component’s contained functions, and then send that data off to your backend server along with the transaction receipt for further processing. My preferred method of linking user identifiers in this way is to store account data in a Redux store, where any component can then fetch those identifiers with Redux’s useSelector hook.

To read up how to link a Redux store to your React components, check out my article dedicated to that subject: Redux Hooks in React: An Introduction.

Sending the receipt along with user identifies, such as an authentication token, to one of your endpoints can be done with a simple fetch request. I have further abstracted the fetch function in the following example as to minimise the boilerplate, but the general idea should be clearly conveyed:

// sending a transaction receipt and identifiers to your serverconst processNewSubscription = async (purchase) => {  const { productId, transactionReceipt } = purchase;
const { authToken } = props;
const { ack, response } = await authFetch({
url: 'iap/receipt-validate',
token: authToken,
body: {
receipt: transactionReceipt,
productId: productId,
}

});
}

authFetch is an imported utility function I have defined to abstract fetch requests, which simply pertains to a fetch request being wrapped in an asynchronous function. This results in a simplified boilerplate to make a simple fetch request. ack is the status code returned from the server (success, failure) whereas response is the JSON response object.

The main takeaway here is that authToken is being sent along with transactionReceipt and productId to the backend server (an Express endpoint in this example) Where this data can be persisted.

It's worth noting that react-native-iap adopts a queue system whereby transactions are lined up for processing and persist in the queue until they are successfully processed. Continued attempts will be made to successfully complete a transaction if the device experiences connectivity problems, that are usually retried when the app is re-opened. For this reason, do not request that a user make another purchase if there is no connectivity.

With a transaction receipt obtained with the vital metadata needed to manage a transaction, lets now turn our attention to the server-side for receipt validation and persistence.

Handling Transaction Receipts Server-Side

This section will explore receipt validation and persistence on an Express server that saves the data of interest to a MongoDB collection. Here are some useful dependencies this section has adopted to get this validation and persistence working:

  • moment: Timestamps are easily generated with moment simply by calling moment().unix(). This requires a lot less boilerplate than achieving the same result with vanilla JavaScript
  • mongodb: The official NodeJS Mongo drivers, that provide a streamlined asynchronous set of APIs to interact with MongoDB instances, either locally or remotely. The document based collections are a well suited match for storing the JSON-based objects of transaction data, and API / webhook data in general.
  • node-apple-receipt-verify: This package provides a simple API for contacting Apple servers for receipt validation.

Here is a handy command for installing all these dependencies:

yarn add moment mongdb node-apple-receipt-verify

Once installed, initialise the Node Apple Receipt Verify service like so:

// initialise node-apple-receipt-verifyvar appleReceiptVerify = require('node-apple-receipt-verify')appleReceiptVerify.config({
secret: process.env.APPLE_SHARED_SECRET,
environment: [process.env.APPLE_APP_STORE_ENV],
excludeOldTransactions: true,
});

Initialise this object outside of your endpoints — we do not want a build up of initialisations on every request to an endpoint. Also, it is good practice to use environment variables where-ever possible for sensitive data such as secret keys.

Now, let’s firstly assume that we’ve delivered the receipt and user info to our Express route, ready for validation:

// receipt validation endpointrouter.post('/receipt-validation', async function (req, res, next) {const { body } = req;
const { authToken, receipt } = body;
...
}

Validating a receipt returns a list of purchased products throughout the lifetime of the subscription. The list of products will be empty if none have been purchased. Furthermore, an exception will be raised if the receipt is invalid:

// receipt validation workflowtry {  // attempt to verify receipt
const products = await appleReceiptVerify.validate({
excludeOldTransactions: true,
receipt: receipt
});
// check if products exist
if (Array.isArray(products)) {

// get the latest purchased product (subscription tier)
let { expirationDate } = products[0];
// convert ms to secs
let expirationUnix = Math.round(expirationDate / 1000);
// persist in database
/* coming up next */
}
} catch(e) {
// transaction receipt is invalid
}

The excludeOldTransactions field of validate() can be set to true to ignore the entire purchase history and only fetch the latest purchase made. The full extent of the returned product object is documented here on NPMJS.

The above snippet firstly attempts to verify the receipt, and handles an exception in the event the receipt cannot be verified. If this is the case, return a message to your app stating there was an error. A returned failed status could be used to make further state updates such as rolling back the post-subscription based updates mentioned earlier.

Upon a successful validation, we firstly check whether purchased products exist, before referencing the latest purchase and corresponding expiry timestamp. This expiry timestamp represents the time of next renewal that is dependant on the subscription period, whether it be weekly, monthly or a larger renewal period.

Since we are working with collection based documents, you may wish to extract more fields from the receipt validation stage and update other collections with the current state of the subscription:

// aggregating transaction data throughout collectionsusers:
plan:
autoRenew
nextRenewal
planId
iap-receipts:
latest_receipt
latest_receipt_info
verified
timestamp
products
iap-logs
receipt_id
times_verified
...

This sorting of data in a range of collections is a common practice for optimisation purposes. For example, you may have metadata existing in an accounts collection with the current state of a user’s subscription:

// example `user` record with subscription metadata{
_id: ObjectId(...),
email: ross@jkrbinvestments.com,
name: 'Ross',
plan: {
freeTrialEligible': false,
planId': 1,
autoRenew': true,
nextRenewal': 1594014143,
}

};

I tend to use the plan term more-so than subscription for data persistence purposes. The definitions of both are the same in relation to in-app purchases.

Storing duplicated data is purely for convenience and optimisation purposes that will make a difference at scale — with the user’s plan data present in a users collection, there is no need to make a second query to an iap-receipts collection to find that same state. This is a common practice for document-based collections, and larger apps will have a number of services for the sole purpose of data aggregation (not to be confused with MongoDBs built-in aggregation feature).

If the reader is interested in data aggregation pipelines, I have posted an article showcasing a use case of MongoDBs Aggregation feature and how it can be integrated within React Native: React Native: MongoDB Aggregation with Stack Navigation.

The following is an example query of inserting a transaction receipt in a standalone iap-receipts MongoDB collection, along with vital metadata pertaining to the user and time the transaction was inserted, as well as whether the receipt was successfully verified:

// insert receipt and products recordawait db
.collection('iap-receipts')
.insertOne({
user_id: user._id,
receipt: receipt,
verified: true,
products: products,
timestamp: moment().unix(),
environment: process.env.APPLE_APP_STORE_ENVIRONMENT,
});

Storing the environment (sandbox or production) may also be useful. Subscription webhooks do the same thing to differentiate what notifications pertain to what environment.

And with this, we have successfully persisted a transaction receipt with a particular app user. This record will be critical in your subscription management going forward, such as updating a user account if the user in question updates or cancels their subscription.

Here are the important points of this setup to keep in mind:

  • Apple will require a transaction receipt of a subscription to identify the in-app purchase and its corresponding subscription history, and will never present user identifiable data.
  • Your app requires a user identifier linked to a transaction receipt in order to update the user’s privileges based on the state of the subscription. It is the developer’s responsibility to ensure this is setup with the initial transaction receipt.

In Summary

This article paves the necessary steps developers should take when storing an in-app purchase transaction receipt. These practices will make automatic renewal or cancellation possible, as well as more advanced Subscription webhook integration.

I have also published a solution to automatically sync subscription renewals and cancellations to your app database using a NodeJS based runtime to do so — this article provides a continuation of what has been discussed here, as a means to leverage how receipts have been persisted with a particular user. Read the full article here:

Here are some other materials that may be of interest that relate to in-app purchase management:

  • Apple’s receipt verification documentation can be found here.
  • The full response body of a receipt can be found here, documenting all the metadata associated with a transaction receipt.
  • Apple’s “What’s new with in-app purchases” presentation from WWDC 2020 can be watched here, where they present the most recent updates to in-app purchases and their surrounding APIs and webhook support.

Written by

Programmer and Author. Director @ JKRBInvestments.com. Creator of ChineseGrammarReview.app for iOS.

Get the Medium app