In App Purchases and Subscriptions in React Native: 2021 Walkthrough
How to implement Expo In App Purchases and Subscriptions for iOS and Android
In-app purchases and subscriptions in 2021: Time for a change?
After more than a year of using react-native-iap as my React Nativ e in-app purchase solution, I recently decided to migrate to expo-in-app-purchases to resolve a number of issues I was facing with the former package.
It is evident from my development journey that expo-in-app-purchases is the better package to adopt now in 2021. I will document the reasons why in this piece, along with a complete implementation walkthrough of expo-in-app-purchases. What will be covered:
- Benefits of
expo-in-app-purchasesand numerous issues I was facing withreact-native-iapwill firstly be explained. - Installation of
expo-in-app-purchaseswill be covered before delving into integrating the package in React Native projects. - Best practices for testing in-app purchases and subscriptions will be covered, as well as common issues that you may stumble upon as you are implementing in-app purchasing for your apps. These issues are based on my own experiences working with the packages for both Android and iOS devices.
- A GitHub Gist containing a sample implementation of
expo-in-app-purchaseshas been provided and will be explained further down.
Expo In App Purchases vs React Native IAP
A comparison of react-native-iap and expo-in-app-purchases will firstly be covered alongside the difficulties I was facing working with the former package.
The installation numbers for the two package may tell a rather misleading story, with react-native-iap currently enjoying over 20,000 weekly downloads at the time of writing and expo-in-app-purchases lagging in comparison with just over 1,500 weekly downloads.
Both packages offer a very similar API structure, relying on an asynchronous connection to the store in question (App Store or Google Play) and an event listener to handle purchase requests for both one-off product purchases and subscription requests.
The similarity in structure results in an easy migration as your code will largely follow identical logic. In fact, the biggest difference is that the expo-in-app-purchases getProductsAsync() method returns a different data structure than the react-native-iap method of the same name.
Although this change has an impact on your data structures and IAP-centric components, the amendments are rather trivial and can be quickly addressed.
Expo In App Purchases works with any React Native Project
expo-in-app-purchases is not exclusive to just Bare Expo Workflows — it works with any React Native project provided that the react-native-unimodules dependency is also installed alongside expo-in-app-purchases.
If you do not use Expo, the react-native-unimodules installation instructions for iOS and Android can be found here on Expo’s website.
Why I migrated to Expo In App Purchases
The major benefits of using expo-in-app-purchases are two-fold:
- No setup is required from Android Studio apart from ensuring a
BILLINGpermission is included in your Manifest file and that you have React Native unimodules installed. iOS simply requires the standard pod install and In-App Purchases Xcode capability added to your project. - All the APIs are reliable and work without issue, with a comprehensive API reference hosted in one coherent GitHub doc.
This reliability and clear documentation makes for a quick and easy expo-in-app-purchases integration.
react-native-iap on the other hand has numerous problems that range from inconsistent documentation, sporadic release cycles and broken APIs, that ultimately prompted my decision to endeavour into alternative IAP solutions.
Here are some of the issues I personally faced with react-native-iap:
- The documentation is very inconsistent with release cycles and is not developer friendly. It is very hard to find working examples of the latest APIs and to debug problems that are often hard to identify due to the poor documentation.
react-native-iapis now on version 6 with no notable changes from version 4 — the version I was using before updating to 6. A4.4.xrelease prevented me from upgrading from4.3.0due to subscription requests simply not working, to which there was no solution found on GitHub.- There are Android installation instructions hosted in the documentation, but having followed these instructions on version 6 of the package, fetching subscriptions were simply not working for Android — with no apparent build errors or inconsistencies.
- There are 101 outstanding Issues on GitHub at the time of writing, many of which have been outstanding for a number of months. Many of these issues pertain to critical functions that IAPs absolutely require to work.
There are evidently a few good reasons to spend a couple of hours to migrate away from react-native-iap. Is there a better solution with expo-in-app-purchases? Yes, and developers can be confident that the package will be maintained in a reliable manner inline with the rest of the Expo suite.
If you’ve experienced similar issues with your IAP implementation or are implementing IAPs in React Native for the first time, the next sections will fast-track that implementation process.
Expo IAP: iOS and Android Installation
Install expo-in-app-purchases with npm or yarn as a project dependency:
yarn add expo-in-app-purchasesIf you are using Expo, make sure you are using the Bare Workflow. In-app purchases currently only work in Bare Workflows due to the native configuration needed within XCode and Android Studio.
If you are not working with an Expo project, it is a good idea at this stage to install react-native-unimodules. Check out the installation steps to ensure it is configured correctly in your native projects before continuing.
Installation for iOS
For the iOS side simply run pod install:
npx pod-install
#or
cd ios && pod installFor your Xcode setup, ensure that the In-App Purchases capability has been added to your project, and that your SKUs have been set up on App Store Connect.
For detailed App Store Connect instructions setting up in-app purchases on the iOS side, refer to the Configuring iOS In-App Purchases section my published article: React Native: Subscriptions with In-App Purchases. The reader is invited to refer to the in-depth instructions from that article in order to save repetition here.
This article will also consider Android and Google Play setup; this will be covered in more detail next.
Installation for Android
Ensure you have a BILLING permission included in your AndroidManifest.xml file. If it is not included simply paste the following tag into the file under the main <manifest> tag:
<uses-permission android:name="android.permission.BILLING" />Some additional setup is also required in your Google Play Console. To set up your in-app purchases, follow these steps:
Google Play Console Setup
Registering as a developer on Google Play requires a one-off $25 fee. Once paid, you have instant access to your account. This one-off fee model is a lot more economical than Apple’s yearly subscription fee of $99.
- To follow along with the following steps, ensure that you have already set up your Google Play developer account at Google Play Console.
Everything from your store listing page, company information, billing and in-app purchases, app testing, and more take place from The Google Play Console. You can now create your app and configure your IAPs.
- Before configuring your app, set up your payments profile to make it possible to receive your earnings from your In-app sales. From the All Apps page (the top-most page of your console dashboard), expand the Setup menu item and click Payments profile to do so. There is a waiting period of a couple of days while a small amount is deposited into your bank account to verify your account.
- From the All Apps page, click Create app and fill in the required details. All your IAPs are hosted from within your App record.
- Go to your App’s Dashboard and find the Monetize menu from the main menu. You will be interested in the In-app products and Subscriptions sections. Go ahead and create your needed SKUs along with their price, billing period and other settings such as free trial conditions.
Metadata like free trial status, currency code and other critical data relating to an in-app purchase or subscription are returned when getProductsAsync() or purchaseItemAsync() is called from expo-in-app-purchases.
Background reading on Google Play IAPs
There is some useful official documentation on the Android Developer website for further background reading on IAPs. The articles of particular interest as pertaining to this piece are Sell digital purchases with Google Play’s billing system and Sell subscriptions with Google Play’s billing system.
Both one-off purchases and subscriptions are supported with the expo-in-app-purchases API, with both types of purchases using unified API calls for both types.
With your IAPs and / or subscriptions now configured we can turn our attention to React Native. But before doing so there lies one app testing detail that impacts how you test In-app purchases:
In-app purchases can only be tested in closed or open testing tracks
If you are new to Android development testing tracks may be a new concept.
There are some useful official docs found here that clearly explain the differences between these three tracks, and that go into a lot more detail than our IAP focus here.
Concretely, releases can be issued to 3 categories, or “testing tracks”:
- Internal testing allows you to build your app and share a testing link internally to your team members. These app builds are un-reviewed, but testers get instant access to the app for immediate testing. The downside to internal testing is that in-app purchases cannot be tested. SKUs will be returned in React Native, but purchase requests will fail.
- Closed and open testing tracks on the other hand require a review from Google, which can take up to 7 days for new developers and 1–3 days for previously reviewed apps (these are typical times cited from Stack Overflow). I typically experience very quick reviews for app updates, typically a few hours. Once successfully reviewed, your app will be available to download to eligible testers via a link given in your Google Play Console. In-app purchases will be available to test from these builds, and that is what we want.
This limitation causes friction in your development cycle, but this friction cannot be avoided. Keep in mind that:
- We can not rapidly test Google Play In-app purchases in development mode with luxuries like hot reloading and instant updates. We must upload a release build of the app to Google Play and allow Google to review the app before we can test IAPs on-device.
- Therefore, and perhaps obviously, in-app purchases can only be tested on a physical Android device, and not in the Android Emulator. Once IAP requests are tested and working to your requirements, further updates to your UI can be done in development mode or internal testing.
With all that being said, and your SKUs now set up for both iOS and Android, let’s move onto the actual React Native implementation.
Implementing Expo In App Purchases in React Native
Implementing IAPs with expo-in-app-purchases is rather straight forward. The documentation lists the needed methods in order of execution. The official documentation includes useful code snippets for all the methods, so this piece will focus on integrating them into a React Native component rather than repeating the examples.
Saying that, here is an overview of the required methods — methods we have to implement — to get in-app purchases working:
connectAsync(): Connects to either the App Store or Google Play store.getProductsAsync([skus]): Fetches the product details of the provided SKUs from the selected store.getPurchaseListener(purchase => { /* callback function */ }): The event listener that handles incoming purchase requests. We will discuss how to implement this next.purchaseItemAsync(sku, ?currentSku): The key function to initiating a purchase. ThecurrentSkuparameter is Android-only needed if the user has an already-active subscription. Calling this function will invoke the native device UI for the purchase, which usually just involves authenticating and confirming the purchase. YourpurchaseEventListenercallback processes and finished the transaction thereafter.finishTransactionAsync(purchase, consume): This function must be called to “finish” the transaction; it marks the transaction as completed Store-side. Thepurchaseparameter will be supplied via your callback function. consume flags whether this is a one-time purchase or a repeatable purchase (subscription).
These are the 5 functions we cannot afford to leave out for IAPs to work. Just like connectAsync(), we also have disconnectAsync() that terminates the connection to the store. This could be called when users no longer have access to in-app purchases, such as when they are signed out and need to be authenticated to purchase items.
If you wish to display purchase history by fetching it directly from the Store-side, you can use getPurchaseHistoryAsync(). This may be useful if you do not persist purchases to your own database — but it is very much recommended that you do so! To also fetch the last response code from the user’s purchase activity, use getBillingResponseCodeAsync().
If you’re coming from react-native-iap, you’ll notice the resemblance between the two APIs that make migrating very straight forward.
Integrating IAP Methods into a React Native Component
Now let’s lay out how exactly these methods can be integrated into React Native. I will document my preferred method; it works well for me but may not be the only effective solution. A fully-commented GitHub Gist will be presented after an overview of the solution.
In my experience the most efficient way to implement the above methods in React Native is to do the following:
- Create a component that connects to the Store and hosts your purchase event listener and callback function, and wrap your component tree that require access to the
expo-in-app-purchasesAPI with this component. This component could be called<IAPManager>for example. This article will stick to this name. - Within this component define some local state to flag whether an in-app purchase is processing. This can simply be a
[processing, setProcessing]pair withuseState. Only one in-app purchase can be processed at one time, and you do not want to allow users to repeatedly request purchases when another is in the middle of processing. - Define a context in the same file to access
<IAPManager />’sprocessingstate as well as other useful functions likegetProductsAsync(). A context hook, calleduseIap()for example, could be defined for functional components that is standard practice these days. - Now within
<IAPManager />, define your purchase logic within thepurchaseItemAsync()function, as well as any processing that is needed on your server. ThefinishTransactionAsync()should be called after your own processing steps.
With this boilerplate set up, child components to <IAPManager /> relying on IAP functionality can leverage your context and invoke purchaseItemAsync(). As these are child components, <IAPManager />’s connectAsync() would have already been called and a connection will be in place to process purchases.
purchaseItemAsync() can be triggered with a simple onPress() from a <Button /> or <TouchableOpacity /> component to get the purchase flow started.
The full IAPManager Component
Below is a GitHub Gist demonstrating how the IAPManager can be defined. I have commented it as much as possible to give the reader as much direction as possible within the source code:
Common Issues and Implementation Tips
This section covers some implementation tips and common issues I have found when working with IAPs.
Price formatting
Be aware of the data structures of both the returned in-app products and purchase objects. If you are coming from react-native-iap, the IAPItemDetails is very different.
Be aware that priceAmountMicros can be converted into a standard price format with the following:
// priceAmountMicros to priceAmountlet priceAmount: number = parseInt(item.priceAmountMicros) / 1000000;
priceAmountMicros can be combined with priceCurrencyCode if you need to manipulate the price (such as working out the monthly outgoing in a 12 month subscription period). Such manipulation is not as easy with the price string property that combines the price and currency code.
Set up an IGNORE_IAP flag
As we ascertained earlier, IAPs can only be tested on physical devices on Android, where only builds from the Closed and Open testing track have access to them.
On the iOS side, product SKUs are not fetched in the Simulator — at least at the time of writing this piece. So if you have a getProductsAsync() when your app starts up — well, your app will be hanging as the method will not resolve or timeout without custom logic in place.
However, we cannot simply define logic that ignores IAPs completely if you are in development mode, with something like process.env.NODE_ENV, because we can still fetch products and test UI pertaining to IAPs in development mode, even if only on a physical device.
Instead of environment, it may be worth setting up an IGNORE_IAP constant if you know you only have access to simulators and emulators. Set this boolean in a constants file and import it where necessary, and toggle the value depending on your testing capabilities.
Checking if you have already connected to the store
If you want to fetch products outside of <IAPManager />, such as when your app starts up as to immediately fetch and store IAPs, it is wise to check if you are already connected to the store.
You could define a connectAsync() within a try catch block to simply ignore failed connection attempts if the store is already connected:
// connect to store if not done so alreadytry {
await InAppPurchases.connectAsync();
} catch (e) { /* already connected, check `e` to verify this */ }
Revert processing state if purchase doesn’t go ahead (but does not fail)
In the official example of setPurchaseListener, an if-statement considers a range of IAPResponseCode values, such as a user cancelling or deferring a purchase, and these may be tempting places to put setProcessing(false) to update your UI after an unsuccessful purchase.
But there actually is an edge case where IAPResponseCode returns OK but your purchase array will be empty. I experienced this on Android when I was already subscribed, but attempted to re-subscribe with the same SKU. The response was OK, but no purchases were returned. The Android UX displayed an already subscribed message, before I closed the IAP modal.
The results.forEach() logic was therefore not be executed, but neither was any other response code handling.
To avoid these edge cases, you can simply put setProcessing(false) after this big if-statement that handles each error code. That is what has been done in the GitHub Gist.
Checking if a user has already subscribed on another platform
This is an edge case, but users with both iOS and Android devices may wish to use your app on both of them. If a user has already subscribed on one platform, you don’t want to allow them to try to subscribe again on another. Instead, you should display a message notifying them that they’ve subscribed elsewhere, and to either manage your subscription on that platform, or contact support to cancel your subscription.
To check for this scenario, store in your database the platform used when the user subscribed. Simply use React Native’s Platform.OS to retrieve this.
Now within your UX components you can check whether this platform is inline with the currently active platform:
// get platform user subscribed from (your database)
const subscribedOn = user.subscription.platform;
// check whether user is now on different platform
const subscribedOnDifferentPlatform = planPlatform !== Platform.OS);
JDK Runtime 16 Issue (Android Only)
As an additional issue worth mentioning that relates your Android Bare Workflow Expo project (not IAP specific), JDK 16.0.1 currently results in an app build failure.
If JDK updates do not play nice, simply amend your android run script in your package.json. The following used JDK 15.0.2 instead of 16.0.1 I had updated on my system:
# package.json"scripts": {
"android": "export JAVA_HOME=$(/usr/libexec/java_home -v 15.0.2); react-native run-android",
...
}
...
This may be fixed by the time the reader is seeing this, but it is worth noting that it is easy to revert to previous JDK version.
In Summary
This wraps up this discussion on implementing in-app purchases using expo-in-app-purchases. This piece discussed the issues with react-native-iap and the reasons for migrating to expo-in-app-purchases as your React Native IAP solution.
With similar APIs, consistent documentation, and reliability, expo-in-app-purchases is the go-to React Native IAP package in 2021, and migrating is a straight forward and worthwhile process.
