
Tutorial
October 27, 2022
25 min read
January 11, 2022
34 min read
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.
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:
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, A/B testing for paywalls, promo campaigns with flexible segmentation, third-party analytics tool integrations, and more.
For now, let’s talk about setting up in-app purchases in React Native apps. Here’s what we’re going to cover today:
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.
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“.
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 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:
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.
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.
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).
Click Save & publish.
That’s it for the configuration. Now, we’ll be adding the dependencies and writing the code.
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!
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 than 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.
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.
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<AdaptyPaywall[]>([]);
useEffect(() => {
async function fetchPaywalls(): Promise<void> {
await activateAdapty({ sdkKey: "MY_PUBLIC_KEY" });
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
fetchPaywalls();
}, []);
return (
<View style={styles.container}>
<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...
}}
/>
</View>
);
};
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:
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<void>;
}
export const Paywall: React.FC<PaywallProps> = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<SafeAreaView style={styles.container}>
{paywall.products.map((product) => (
<View key={product.vendorProductId}>
<Text>{product.localizedTitle}</Text>
<Button
title={`Buy for за ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("Error occured :(");
} finally {
setIsLoading(false);
}
}}
/>
</View>
))}
</SafeAreaView>
);
};
// 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 the purchase button next to each product.
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" } }] } },
});
});
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<PaywallProps>({
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<AdaptyPaywall[]>([]);
useEffect(() => {
async function fetchPaywalls(): Promise<void> {
await activateAdapty({
sdkKey: "MY_PUBLIC_KEY",
});
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
fetchPaywalls();
}, []);
return (
<View style={styles.container}>
<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);
// Doing everything we need
console.log("purchase", purchase);
},
},
},
});
}}
/>
</View>
);
};
interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise<void>;
}
export const Paywall: React.FC<PaywallProps> = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<SafeAreaView style={styles.paywallContainer}>
{paywall.products.map((product) => (
<View key={product.vendorProductId}>
<Text>{product.localizedTitle}</Text>
<Button
title={`Buy for ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("Error occured :(");
} finally {
setIsLoading(false);
}
}}
/>
</View>
))}
</SafeAreaView>
);
};
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 with an emulator.
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 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 many 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<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();
}, []);
const renderContent = (): React.ReactNode => {
if (isLoading) {
return <Text>Loading...</Text>;
}
if (isPremium) {
return (
<Image
source={{
uri: "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 a paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such a 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>;
};
interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise<void>;
}
export const Paywall: React.FC<PaywallProps> = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<SafeAreaView style={styles.paywallContainer}>
{paywall.products.map((product) => (
<View key={product.vendorProductId}>
<Text>{product.localizedTitle}</Text>
<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);
}
}}
/>
</View>
))}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
paywallContainer: {
flex: 1,
alignItems: "center",
justifyContent: "space-evenly",
backgroundColor: PlatformColor("secondarySystemBackground"),
},
});
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 to delving deeper into store kits – stay tuned for more. Thanks!
Further reading
Tutorial
October 27, 2022
25 min read
Trends-insights
October 7, 2021
10 min read