React native in-app purchases: simple implementation. Tutorial

Murat Menzilci
14 min readApr 14, 2022

Cross-platform app development frameworks certainly make developers’ lives easier, allowing them to build apps for multiple platforms at once. There are some downsides, though. For example, React Native has no ready-made tool for implementing in-app purchases. Therefore, you’ll inevitably have to turn towards third-party libraries.

We’ve just created a Discord server to help app developers, marketers and products managers to learn, share and educate about app business. You can join us! https://go.adapty.io/join-discord

What options there are for in-app purchases implementation

Popular libraries for in-app subscriptions in React Native apps are react-native-iap and expo-in-app-purchases. I’ll talk about react-native-adapty, though, because there are quite a few benefits to it, compared to the other libraries:

  • Unlike those, it provides server-based purchase validation.
  • It supports all the features recently implemented by the app stores, all the way from promo offers to pay-upfront features. It’s also quick to support new features to come.
  • The code ends up being more clear and straightforward.
  • You can modify your product offering and add or remove new offers without having to go through the full release cycle. There’s no need to release beta versions and wait for approval.

There’s much more to the Adapty SDK than that. You get built-in analytics tools for all the key metrics, cohort analysis, server-based purchase validation, AB testing for paywalls, promo campaigns with flexible segmentation, third-party analytics tool integrations, and more.

In this article

For now, let’s talk about setting up in-app purchases in React Native apps. Here’s what we’re going to cover today:

  1. Why Expo won’t work for in-app purchases in React Native apps.
  2. Creating a developer account.
  3. Configuring Adapty:
    Configuring the App Store
    Configuring the Play Store
  4. Adding subscriptions.
  5. Creating a paywall.
  6. Installing react-native-adapty.
  7. A sample app & the result.

In this guide, we’ll try to build an app that displays cat pictures to subscribed users and prompts everyone else with a subscription offer.

Why Expo won’t work for in-app purchases in React Native apps

To cut a long story short: Expo “managed” doesn’t support the native methods app stores offer for purchase processing (also known as store kits). You’ll need to either stick to pure RN or use Expo bare workflow.

Right off the bat, I’ll have to disappoint those who thought of using Expo: this won’t work. Expo is a React Native framework that makes app development much easier. Their managed workflow isn’t compatible with purchase/subscription processing, though. Expo doesn’t use any native code in their methods and components (both are JS-only), which is required for store kits. There’s no way to implement in-app purchases in mobile stores with JavaScript, so you’ll have to “eject”.

Creating a developer account

First, you’ll need to set up app store accounts, as well as create and configure purchases and subscriptions for both iOS and Android. This should hardly take you more than 20 minutes.

If you still haven’t configured your developer account and products in App Store Connect and/or Google Play Console, see these guides:

  • For iOS: read the guide from the beginning and up to the “Getting the list of SKProduct” heading, as it’s where we start discussing native implementations.
  • For Android: read the guide from the beginning and up to the “Getting a list of products in an app” heading.

Configuring Adapty

For react-native-adapty, you’ll first need to configure your Adapty dashboard. This won’t take much time, but will get you all the advantages listed above Adapty has over hard coding.

On the third step, you’ll be prompted with App Store and Google Play configurations.

For iOS, you’ll need to:

  • Specify the Bundle ID;
  • Set up the App Store Server Notifications;
  • Specify the App Store Connect shared secret.

These fields are required for the purchases to work.

Each field has a ‘Read how’ hint that contains step-by-step how-to guides. Check these out if you have any questions.

Bundle ID is the unique ID of your app. It must match the one you specified in Xcode, in Targets > [App Name] > General:

For Android, the required fields are the Package Name and Service Account Key File. All these fields have their own Read how hints as well. Package name does in Android what the Bundle ID does in iOS. It must match the one you specified in your code, which can be found in the /android/app/build.gradle file in android.defaultConfig.applicationId:

On the fourth step, you’ll be prompted with connecting the Adapty SDK to your app. Skip this step for now, though — we’ll get back to it a bit later.

Once you’ve signed up, check out the settings tab and remember that this is where your Public SDK key can be found. You’ll need the key later on.

Adding a subscription

Adapty uses products for different subscriptions. Your cat pics subscription can be weekly, bi-annual, or annual. Each of these options will be a separate Adapty product.

Let’s specify in the dashboard that we have one product. To do so, go to Products & A/B Tests → Products and click Create product.

Here, you’ll need to specify the product name, that is, how this subscription will look in your Adapty dashboard.

You’ll also need to specify the App Store Product ID and Play Store Product ID. If you want, specify the period and the name as well for analytics. Click Save.

Creating a paywall

Now, you’ll need to design a paywall, which is a screen that restricts the user’s access to premium features and prompts them with a subscription offer. You’ll need to add the product you’ve created to your paywall. To do so, click Create paywall in the same section (Products & A/B Tests → Paywalls).

  • Choose such a Paywall name that you and your team will easily be able to infer, just by looking at the name, which paywall it is.
  • You’ll use the Paywall ID to display this paywall in your app. For our sample app, we’ll use “cats_paywall.”
  • In the Product drop-down, select your subscription.

Click Save & publish.

That’s it for the configuration. Now, we’ll be adding the dependencies and writing the code.

Installing react-native-adapty

1. First, add the dependency:

yarn add react-native-adapty

2. Install iOS pods. If you don’t have the CLI pod yet, I strongly recommend you download it. You’ll certainly need it a lot in iOS development.

#pods get installed into the native iOS project, which, by default, is the /ios folderpod install --project-directory=ios

3. Since iOS React Native projects are written in Obj-C, you’ll need to create a Swift Bridging Header so that Obj-C can read Swift libraries. To do that, just open your Xcode project and create a new Swift file. Xcode will ask whether you want to create a bridging header, which is exactly what you want. Click Create.

4. For Android, make sure that the project — /android/build.gradle by default — is using the kotlin-gradle-plugin of version 1.4.0 or above:

... buildscript { 
... dependencies {
... classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0"
}
} ...

5. For Android, you’ll need to enable multiDex, which can be found in the app’s config file (/android/app/build.gradle by default.)

... 
android {
... defaultConfig {
... multiDexEnabled true
}
}

Voila, you’re all set and can get some rest start coding! 🎉

Retrieving the product list in the app

There are tons of useful things happening under react-native-adapty’s hood. You’ll certainly need these, sooner or later, which is why you should initialize the library at the very beginning of your flow. Go as high as you can in your app’s code (you can do this right in the App.tsx as well) and start the initialization:

// import the method
import { activateAdapty } from 'react-native-adapty';
// We’ve had this App component in our app’s root
const App: React.FC = () => {
...
// we’re invoking it once in a root component on mount
useEffect(() => {
activateAdapty({ sdkKey: 'MY_PUBLIC_KEY' });
},[]);
...
}

Here, replace MY_PUBLIC_KEY with your Public SDK key found in the dashboard settings. Actually, the activateAdapty() method can be invoked more that once and in more than one place, but we’ll stick with this design.

Now, we can retrieve the products we’ve added in the Adapty dashboard:

import { adapty } from 'react-native-adapty';async function getProducts() {
const {paywalls, products} = await adapty.paywalls.getPaywalls();
return products;
}

Now, let’s get to practice: We’ll try to build a small app where we can browse the products from our paywalls and make purchases.

Sample app

I’ll keep things short from here on to avoid making the base logic overcomplicated. I’ll also be coding in TypeScript to show you which types are used and where. For testing, I’ll use my good old iPhone 8. Remember that from iOS 14 on, App Store forbids using store kits in emulators — you can only test using physical devices.

App.tsx root component

1. First, let’s create an App.tsx root component that will have a paywall display button. We’ve already configured the navigation via react-native-navigation — we believe it to be much better than the react-navigation option recommended in the official docs.

ISSUE

import React, { useEffect, useState } from "react";
import { Button, StyleSheet, View } from "react-native";
import { adapty, activateAdapty, AdaptyPaywall } from "react-native-adapty";
export const App: React.FC = () => {
const [paywalls, setPaywalls] = useState([]);
useEffect(() => {
async function fetchPaywalls(): Promise {
await activateAdapty({ sdkKey: "MY_PUBLIC_KEY" });
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
fetchPaywalls();
}, []);
return (

<Button
title="Show the paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such paywall");
}
// Switching to a paywall...
}}
/>

);
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
});

What’s going on here? On mount, the fetchPaywalls() function is invoked. It activates the SDK and saves the paywalls in the state so that the user doesn’t have to wait for fetching after tapping the button. There’s just one button in the view that’s supposed to take the user to the paywall we’ve previously designed in the dashboard.

Actually, it’s possible to fetch the paywalls right here, without saving them into the state. By default, adapty.paywalls.getPaywalls() will fetch them from the cache storage (after caching them on launch), which means you won’t have to wait for the method to talk to the server.

Here’s the result:

A paywall component

2. Let’s write a paywall component in the same file.

// there are more imports here
import React, { useEffect, useState } from "react";
import {
Button,
SafeAreaView,
StyleSheet,
Text,
View,
PlatformColor,
} from "react-native";
import {
adapty,
activateAdapty,
AdaptyPaywall,
AdaptyProduct,
} from "react-native-adapty";
import { Navigation } from "react-native-navigation";
// ...interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise;
}
export const Paywall: React.FC = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState(false);
return (

{paywall.products.map((product) => (

{product.localizedTitle}
<Button
title={`Buy for за ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("Error occured :(");
} finally {
setIsLoading(false);
}
}}
/>

))}

);
};
// A new key
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
paywallContainer: {
flex: 1,
alignItems: "center",
justifyContent: "space-evenly",
backgroundColor: PlatformColor("secondarySystemBackground"),
},
});

Here, we’ll just map the products from the paywall and display a purchase button next to each product.

Registering the screen

3. To see what this looks like, let’s register this screen in react-native-navigation. If you’re using some other navigation, skip this step. My root index.js file looks like this:

import "react-native-gesture-handler";
import { Navigation } from "react-native-navigation";
import { App, Paywall } from "./App";Navigation.registerComponent("Home", () => App);
Navigation.registerComponent("Paywall", () => Paywall);
Navigation.events().registerAppLaunchedListener(() => {
Navigation.setRoot({
root: { stack: { children: [{ component: { name: "Home" } }] } },
});
});

“Display the paywall” button

4. Now, we’ll just have to assign an action to the “Display the paywall” button. In our case, it’ll prompt a modal via Navigation.

Navigation.showModal({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
// Doing everything we need
console.log("purchase", purchase);
},
},
},
});

The entire App.tsx file:

import React, { useEffect, useState } from "react";
import {
Button,
SafeAreaView,
StyleSheet,
Text,
View,
PlatformColor,
} from "react-native";
import {
adapty,
activateAdapty,
AdaptyPaywall,
AdaptyProduct,
} from "react-native-adapty";
import { Navigation } from "react-native-navigation";
export const App: React.FC = () => {
const [paywalls, setPaywalls] = useState([]);
useEffect(() => {
async function fetchPaywalls(): Promise {
await activateAdapty({
sdkKey: "MY_PUBLIC_KEY",
});
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
fetchPaywalls();
}, []);
return (

<Button
title="Show paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such paywall");
}
Navigation.showModal({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
// Doing everything we need
console.log("purchase", purchase);
},
},
},
});
}}
/>

);
};
interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise;
}
export const Paywall: React.FC = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState(false);
return (

{paywall.products.map((product) => (

{product.localizedTitle}
<Button
title={`Buy for ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("Error occured :(");
} finally {
setIsLoading(false);
}
}}
/>

))}

);
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
paywallContainer: {
flex: 1,
alignItems: "center",
justifyContent: "space-evenly",
backgroundColor: PlatformColor("secondarySystemBackground"),
},
});

That’s it! Now, you can display these paywalls to your users.

If you want to test your iOS subscription in a sandbox, you’ll need to create your own sandbox tester account. Keep in mind that sandbox subscriptions are quickly invalidated to make testing easier. For Android, you won’t need any extra accounts — you can even run tests in an emulator.

Checking whether the user has any active subscriptions

We still need to decide where to store active subscription data to grant the end user access to their premium content. Adapty will help us with this as well, as it saves all purchases associated with the user. Let’s do it this way: if the user has no subscription, they’ll be prompted with a paywall button. If they do, we’ll show them a cat picture.

Since active subscription data is retrieved either from the server or cache storage, you’ll need a loader. For the sake of simplicity, let’s add the isLoading and isPremium states.

// ...
export const App: React.FC = () => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isPremium, setIsPremium] = useState<boolean>(false);
const [paywalls, setPaywalls] = useState<AdaptyPaywall[]>([]);
useEffect(() => {
async function fetchPaywalls(): Promise<void> {
try {
await activateAdapty({
sdkKey: "MY_PUBLIC_KEY",
});
const profile = await adapty.purchases.getInfo();
const isSubscribed = profile.accessLevels.premium.isActive;
setIsPremium(isSubscribed);
if (!isSubscribed) {
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
} finally {
setIsLoading(false);
}
}
fetchPaywalls();
}, []);
// ...
}
// ...

Here’s what changed: we’ve added to flags to the state. The entire contents of fetchPaywalls() are now wrapped in a try-catch block so that the code will reach setIsLoading(false) in any possible scenario. To check whether the user has an active subscription, we’re retrieving the user’s profile (which contains all their subscription data) and see the value of profile.accessLevels.premium.isActive. You can use as much access levels (accessLevels) — which are basically just subscription tiers, such as Gold or Premium — as you want, but let’s keep the default value for now. Adapty will create the premium access level automatically, and for most apps, this will be enough. isActive will remain true while there’s an active subscription with this access level

From here on, everything looks quite straightforward. If the user has the premium-tier subscription status, there’s no need to fetch the paywalls — just disable the loader and display the content.

export const App: React.FC = () => {
// ...
const renderContent = (): React.ReactNode => {
if (isLoading) {
return <Text>Loading...</Text>;
}
if (isPremium) {
return (
<Image
source={{
url: "https://25.media.tumblr.com/tumblr_lugj06ZSgX1r4xjo2o1_500.gif",
width: Dimensions.get("window").width * 0.8,
height: Dimensions.get("window").height * 0.8,
}}
/>
);
}
return (
<Button
title="Show paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such paywall");
}
Navigation.showModal<PaywallProps>({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
const isSubscribed =
purchase.purchaserInfo.accessLevels?.premium.isActive;
setIsPremium(isSubscribed);
Navigation.dismissAllModals();
},
},
},
});
}}
/>
);
};
return <View style={styles.container}>{renderContent()}</View>;
};

Here, we’re adding a function that renders the content as well as some logic to onRequestBuy: namely, updating isPremium’s state and closing the modal.

That’s the end result:

The whole file:

import React, { useEffect, useState } from "react";
import {
Button,
SafeAreaView,
StyleSheet,
Text,
View,
PlatformColor,
Image,
Dimensions,
} from "react-native";
import {
adapty,
activateAdapty,
AdaptyPaywall,
AdaptyProduct,
} from "react-native-adapty";
import { Navigation } from "react-native-navigation";
export const App: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [isPremium, setIsPremium] = useState(false);
const [paywalls, setPaywalls] = useState([]);
useEffect(() => {
async function fetchPaywalls(): Promise {
try {
await activateAdapty({
sdkKey: "MY_PUBLIC_KEY",
});
const profile = await adapty.purchases.getInfo();
const isSubscribed = profile.accessLevels.premium.isActive;
setIsPremium(isSubscribed);
if (!isSubscribed) {
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
} finally {
setIsLoading(false);
}
}
fetchPaywalls();
}, []);
const renderContent = (): React.ReactNode => {
if (isLoading) {
return Loading...;
}
if (isPremium) {
return (
);
}
return (
<Button
title="Show a paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such a paywall");
}
Navigation.showModal({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
const isSubscribed =
purchase.purchaserInfo.accessLevels?.premium.isActive;
setIsPremium(isSubscribed);
Navigation.dismissAllModals();
},
},
},
});
}}
/>
);
};
return {renderContent()};
};
interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise;
}
export const Paywall: React.FC = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState(false);
return (

{paywall.products.map((product) => (

{product.localizedTitle}
<Button
title={`Buy for ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("An error occured :(");
} finally {
setIsLoading(false);
}
}}
/>

))}

);
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
paywallContainer: {
flex: 1,
alignItems: "center",
justifyContent: "space-evenly",
backgroundColor: PlatformColor("secondarySystemBackground"),
},
});

To sum everything up

We’ve ended up building a pretty looking and extremely useful subscription app. Those who pay will see cats, and everyone else will get paywalls instead. This guide should have taught you everything you might need to implement in-app purchases in your app.

And for those who’d be looking forward towards delving deeper into store kits — stay tuned for more.

--

--

Murat Menzilci

📱 I write about how to make money with mobile application development, news about App Store and Google Play Store, working as a freelancer and making money. 💰