Outline
In this blog post, I will introduce how to use react-native-iap
to implement IAP(In-App Purchases) on React Native.
Create IAP product
We need to create a product for IAP(In-APp purchases). Let’s see how to make the product on each platform.
Create IAP product on iOS
Agreements, Tax, and Banking
To create IAP product on iOS, we need to configure Agreements, Tax, and Banking
first. To set Agreements, Tax, and Banking
, go to Appstore Connect.
Click the Agreements, Tax, and Banking
on the bottom to move on.
I already registered, so I can see the screen above, but you don’t have Paid Apps
yet, so you need to set it. There are some items that you should insert. And just enter all necessary items, you can get Paid Apps
on here.
Create product on iOS
Next, we need to make a product for IAP on iOS. Click the app which you want to implement IAP.
Click the Manage
under In-App Purchases
on the left bottom, and when you click the plus button(+
), you can see the screen like above that you choose the product to add.
I added Auto-Renewable Subscription
to my app, so I will explain to focus Auto-Renewable Subscription
. When I add other products, I will modify this blog post about them.
In here, Reference Name
is shown up on only Appstore Connect, so choose the name that you can recognize easily. We will use Product ID
to get the product information on the app.
Next, we need to insert Subscription Group Reference Name
. it is also shown up on only Appstore Connect like Reference Name
, so choose the name that you can find easily.
Insert IAP metadata
After creating IAP product item above, we need to add the metadata of the IAP item.
when you click the Manage
menu under In-App Purchases
on the left bottom again, you can see the missing metadata. Click the reference name to go to the product item detail page.
On the detail page, you need to enter the metadata like below.
- Subscription Duration
- Subscription Prices
- Localizations of App Store Information
- Review information
In here, App store information(name and description) will be shown up on the app and Appstore, so be careful to write them. We can get the name and description via react-native-iap. If we don’t show them as they are, it can be rejected.
In order to avoid Reject, I have written the information in detail like how you can see the payment screen and what contents are shown up on the screen.
- Title of publication or service
- Length of subscription (time period and/or content/services provided during each subscription period)
- Price of subscription, and price per unit if appropriate
- Payment will be charged to iTunes Account at confirmation of purchase
- Subscription automatically renews unless auto-renew is turned off at least 24-hours before the end of the current period
- Account will be charged for renewal within 24-hours prior to the end of the current period, and identify the cost of the renewal
- Subscriptions may be managed by the user and auto-renewal may be turned off by going to the user’s Account Settings after purchase
- Links to Your Privacy Policy and Terms of Use
- Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication, where applicable
If your app doesn’t have the contents above, you get the Reject. So, I’ve written the contents above with a screenshot.
Configure permission on iOS
Next, we need to configure the permission on iOS for IAP feature.
Open the project with Xcode, and add In-App Purchase
on Signing & Capbilities
.
Create IAP product on Android
Configure Payments
You need to configure the Payments
to get the payment when the users purchase. To set the Payments
, go to Google Play Console and click Settings
> Developer account
> Payments settings
to configure the Payments
information.
I used and connected Google Adsense
, so I didn’t need to do anything.
Configure permission on Android
To create IAP product, when you go to Google Play Console and click the app that you want to make, and click Subscriptions
menu, you can see the screen like below.
On Android, you need to upload the app which has BILLING
permission to create IAP product. First, open android/app/src/main/AndroidManifest.xml
file and modify it to add BILLING
permission like below.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.github.dev.yakuza.toeicwords">
...
<uses-permission android:name="com.android.vending.BILLING"/>
...
<application
android:name=".MainApplication"
After configuring the permission, upload the app to Google Play.
After uploading, you can see the contents changed on Subscriptions
menu like above.
Create product on Android
Next, click Create subscription
button on Subscriptions
menu.
And then, enter the Subscription information to make a subscription item.
- Product ID
- Subscription details(Name, Description)
- Price(Billing period, Default price)
The Product ID
is for purchasing. Normally use the same Product ID
with iOS in here. You can get the subscription detail, price, etc via react-native-iap by this ID. (I am not sure that the app is rejected, if we don’t display the subscription detail, price on Android like iOS.)
Cording
Install react-native-iap
Execute the command below to install react-native-iap
.
npm install react-native-iap --save
And then execute the command below to install necessary libraries for iOS.
npx pod-install
JavaScript
The code below is that I used for production apps.
import React, { createContext, useState, useEffect, useCallback, useRef, useContext } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
Alert,
EmitterSubscription,
Platform,
Linking,
ActivityIndicator,
AppState,
} from 'react-native';
import Styled from 'styled-components/native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import SplashScreen from 'react-native-splash-screen';
import ENV from '~/env';
import { ThemeContext } from '~/Context';
import { ActionSheet } from '~/Component';
import RNIap, {
InAppPurchase,
SubscriptionPurchase,
finishTransaction,
purchaseErrorListener,
purchaseUpdatedListener,
Subscription,
PurchaseError,
} from 'react-native-iap';
import { checkReceipt } from './checkReceipt';
let purchaseUpdateSubscription: EmitterSubscription;
let purchaseErrorSubscription: EmitterSubscription;
const itemSubs = Platform.select({
default: [ENV.subscriptionId],
});
const Container = Styled.View`
`;
const TitleContainer = Styled.View`
justify-content: center;
align-items: center;
padding: 16px;
`;
const Title = Styled.Text`
font-size: 16px;
font-weight: bold;
color: ${({ theme }: { theme: Theme }): string => theme.black};
`;
const SubTitle = Styled.Text`
font-size: 16px;
color: ${({ theme }: { theme: Theme }): string => theme.black};
`;
const DescriptionContainer = Styled.View`
padding: 0 24px;
width: 100%;
`;
const Description = Styled.Text`
font-size: 14px;
color: ${({ theme }: { theme: Theme }): string => theme.black};
`;
const PurchaseButton = Styled.TouchableOpacity`
margin: 16px;
padding: 16px;
border-radius: 10px;
background-color: ${({ theme }: { theme: Theme }): string => theme.black};
justify-content: center;
align-items: center;
`;
const PurchaseLabel = Styled.Text`
color: ${({ theme }: { theme: Theme }): string => theme.white};
font-size: 16px;
`;
const Terms = Styled.Text`
font-size: 12px;
color: ${({ theme }: { theme: Theme }): string => theme.black};
`;
const Link = Styled.Text`
color: ${({ theme }: { theme: Theme }): string => theme.primary};
`;
const LoadingContainer = Styled.View`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
justify-content: center;
align-items: center;
`;
const LoadingBackground = Styled.View`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${({ theme }: { theme: Theme }): string =>
theme.isLightTheme ? theme.black : theme.white};
opacity: 0.8;
`;
interface Props {
children: JSX.Element | Array<JSX.Element>;
}
const IAPContext = createContext<IAPContext>({
isSubscription: false,
subscription: undefined,
showPurchase: () => {},
});
const IAPProvider = ({ children }: Props): JSX.Element => {
const [showLoading, setShowLoading] = useState<boolean>(false);
const [isSubscription, setIsSubscription] = useState<boolean>(false);
const [subscription, setSubscription] = useState<Subscription | undefined>(undefined);
const actionSheetRef = useRef<ActionSheet>(null);
const { theme } = useContext<ThemeContext>(ThemeContext);
const showPurchase = () => {
actionSheetRef.current?.open();
};
const _checkReceipt = async () => {
const isValidated = await checkReceipt();
setIsSubscription(isValidated);
setTimeout(() => {
SplashScreen.hide();
}, 1000);
};
const _requestSubscription = () => {
setShowLoading(true);
if (subscription) {
RNIap.requestSubscription(subscription.productId);
}
};
const _restorePurchases = () => {
setShowLoading(true);
RNIap.getAvailablePurchases()
.then((purchases) => {
console.debug('restorePurchases');
let receipt = purchases[0].transactionReceipt;
if (Platform.OS === 'android' && purchases[0].purchaseToken) {
receipt = purchases[0].purchaseToken;
}
AsyncStorage.setItem('receipt', receipt);
setShowLoading(false);
setIsSubscription(true);
Alert.alert(
ENV.language['restore successful'],
ENV.language['you have successfully restored your purchase history'],
[{ text: ENV.language['ok'], onPress: () => actionSheetRef.current?.close() }],
);
})
.catch((err) => {
console.debug('restorePurchases');
console.error(err);
setShowLoading(false);
setIsSubscription(false);
AsyncStorage.removeItem('receipt');
Alert.alert(ENV.language['restore failed'], ENV.language['restore failed reason']);
});
};
const _initIAP = useCallback(async (): Promise<void> => {
RNIap.clearProductsIOS();
try {
const result = await RNIap.initConnection();
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
if (result === false) {
Alert.alert(ENV.language["couldn't get in-app-purchase information"]);
return;
}
} catch (err) {
console.debug('initConnection');
console.error(err.code, err.message);
Alert.alert(ENV.language['fail to get in-app-purchase information']);
}
purchaseUpdateSubscription = purchaseUpdatedListener(
(purchase: InAppPurchase | SubscriptionPurchase) => {
console.debug('purchaseUpdatedListener');
setShowLoading(false);
setTimeout(() => {
actionSheetRef.current?.close();
}, 400);
const receipt =
Platform.OS === 'ios' ? purchase.transactionReceipt : purchase.purchaseToken;
if (receipt) {
finishTransaction(purchase)
.then(() => {
AsyncStorage.setItem('receipt', receipt);
setIsSubscription(true);
})
.catch(() => {
setIsSubscription(false);
Alert.alert(
ENV.language['purchase is failed'],
ENV.language['the purchase is failed'],
);
});
}
},
);
purchaseErrorSubscription = purchaseErrorListener((error: PurchaseError) => {
console.debug('purchaseErrorListener');
console.error(error);
setShowLoading(false);
if (error.code !== 'E_USER_CANCELLED') {
Alert.alert(ENV.language['purchase is failed'], ENV.language['the purchase is failed']);
}
});
const subscriptions = await RNIap.getSubscriptions(itemSubs);
setSubscription({
...subscriptions[0],
});
}, []);
const handleAppStateChange = (nextAppState: string): void => {
if (nextAppState === 'active') {
_checkReceipt();
}
};
useEffect(() => {
_initIAP();
_checkReceipt();
AppState.addEventListener('change', handleAppStateChange);
return (): void => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
}
if (purchaseErrorSubscription) {
purchaseErrorSubscription.remove();
}
if (handleAppStateChange) {
AppState.removeEventListener('change', handleAppStateChange);
}
};
}, []);
return (
<IAPContext.Provider
value={{
isSubscription,
subscription,
showPurchase,
}}>
{children}
<ActionSheet
ref={actionSheetRef}
height={Platform.OS === 'ios' ? 380 : 420}
disableClose={showLoading}
customStyles={{
container: {
backgroundColor: theme.white,
},
}}>
{subscription && (
<Container>
<TitleContainer>
<Title>{subscription.title.split(' (')[0]}</Title>
<SubTitle>{subscription.description}</SubTitle>
</TitleContainer>
<DescriptionContainer>
<Description>
<Icon name="information-outline" />{' '}
{
ENV.language[
"I'm developing the app alone. So your subscription is a great help to me"
]
}
</Description>
<Description>
{' '}
{ENV.language['Your subscription helps me keep the good words app']}
</Description>
<Description>
{' '}
{
ENV.language[
'Learn vocabulary without ads for the price of a cup of coffee each month'
]
}
</Description>
</DescriptionContainer>
<PurchaseButton
onPress={() => {
_requestSubscription();
}}>
<PurchaseLabel>
{subscription.localizedPrice} / {ENV.language['month']}
</PurchaseLabel>
</PurchaseButton>
<DescriptionContainer>
<Terms>
- {ENV.language['Already subscribed?']}{' '}
<Link onPress={() => _restorePurchases()}>
{ENV.language['Restoring purchases']}
</Link>
</Terms>
<Terms>- {ENV.language['cancel the purchase']}</Terms>
{Platform.OS === 'ios' && (
<Terms>- {ENV.language['payment is charged to your iTunes account']}</Terms>
)}
<Terms>
- {ENV.language['If you have any question,']}{' '}
<Link onPress={() => Linking.openURL('https://deku.posstree.com/ko/contact/')}>
Contact
</Link>{' '}
{ENV.language['us']}
</Terms>
<Terms>
- {ENV.language['see the']}
<Link onPress={() => Linking.openURL('https://deku.posstree.com/privacy/ko/')}>
{ENV.language['terms of use']}
</Link>{' '}
{ENV.language['details']}
</Terms>
</DescriptionContainer>
</Container>
)}
{showLoading && (
<LoadingContainer>
<LoadingBackground />
<ActivityIndicator color={theme.primary} size="large" />
</LoadingContainer>
)}
</ActionSheet>
</IAPContext.Provider>
);
};
export { IAPProvider, IAPContext };
The source code above is dependent on my project, so you can’t use it directly. Also, I use Context API
to implement the payment logic. Let’s see the code one by one to understand the payment logic.
Mainly, We can divide the payment logic initialization
, payment
, validation
, and restore
.
Initialization
First, we need to initialize the library, and get the product information.
...
const itemSubs = Platform.select({
default: [ENV.subscriptionId],
});
...
const IAPProvider = ({ children }: Props): JSX.Element => {
...
const _initIAP = useCallback(async (): Promise<void> => {
RNIap.clearProductsIOS();
try {
const result = await RNIap.initConnection();
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
if (result === false) {
Alert.alert(ENV.language["couldn't get in-app-purchase information"]);
return;
}
} catch (err) {
console.debug('initConnection');
console.error(err.code, err.message);
Alert.alert(ENV.language['fail to get in-app-purchase information']);
}
...
const subscriptions = await RNIap.getSubscriptions(itemSubs);
setSubscription({
...subscriptions[0],
});
}, []);
...
useEffect(() => {
_initIAP();
...
}, []);
return (
...
);
};
...
To initialize the library, we can use the code below.
RNIap.clearProductsIOS();
try {
const result = await RNIap.initConnection();
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
if (result === false) {
Alert.alert(ENV.language["couldn't get in-app-purchase information"]);
return;
}
} catch (err) {
console.debug('initConnection');
console.error(err.code, err.message);
Alert.alert(ENV.language['fail to get in-app-purchase information']);
}
The clearProductsIOS
function and flushFailedPurchasesCachedAsPendingAndroid
function are dependent the platform, but we don’t need to use Platform.OS
to separate the logic in here.
And then, we need to set the product ID to get the subscription product information.
const itemSubs = Platform.select({
default: [ENV.subscriptionId],
});
I use the same ID between iOS and Android, so I just set the default, if the iOS ID and Android ID are different, you can set it like below.
const itemSubs = Platform.select({
ios: [ENV.ios.subscriptionId],
android: [ENV.android.subscriptionId],
});
You can get the detail of the product by this product ID.
const subscriptions = await RNIap.getSubscriptions(itemSubs);
setSubscription({
...subscriptions[0],
});
And store it on State
. In my case, I have just one subscription item, so I just use subscriptions[0]
to store one item.
And then, display the information on the screen like below.
...
<Title>{subscription.title.split(' (')[0]}</Title>
<SubTitle>{subscription.description}</SubTitle>
...
<PurchaseLabel>
{subscription.localizedPrice} / {ENV.language['month']}
</PurchaseLabel>
...
Payment
Next, let’s see the payment logic to use the product information stored above.
...
let purchaseUpdateSubscription: EmitterSubscription;
let purchaseErrorSubscription: EmitterSubscription;
...
const IAPProvider = ({ children }: Props): JSX.Element => {
...
const _requestSubscription = () => {
setShowLoading(true);
if (subscription) {
RNIap.requestSubscription(subscription.productId);
}
};
...
const _initIAP = useCallback(async (): Promise<void> => {
...
purchaseUpdateSubscription = purchaseUpdatedListener(
(purchase: InAppPurchase | SubscriptionPurchase) => {
...
const receipt =
Platform.OS === 'ios' ? purchase.transactionReceipt : purchase.purchaseToken;
if (receipt) {
finishTransaction(purchase)
.then(() => {
AsyncStorage.setItem('receipt', receipt);
setIsSubscription(true);
})
.catch(() => {
setIsSubscription(false);
Alert.alert(
ENV.language['purchase is failed'],
ENV.language['the purchase is failed'],
);
});
}
},
);
purchaseErrorSubscription = purchaseErrorListener((error: PurchaseError) => {
console.debug('purchaseErrorListener');
console.error(error);
setShowLoading(false);
if (error.code !== 'E_USER_CANCELLED') {
Alert.alert(ENV.language['purchase is failed'], ENV.language['the purchase is failed']);
}
});
...
}, []);
...
useEffect(() => {
...
return (): void => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
}
if (purchaseErrorSubscription) {
purchaseErrorSubscription.remove();
}
...
};
}, []);
...
};
...
We need to register the Listener
to get the success or fail of the payment after purchasing.
...
let purchaseUpdateSubscription: EmitterSubscription;
let purchaseErrorSubscription: EmitterSubscription;
...
purchaseUpdateSubscription = purchaseUpdatedListener(
(purchase: InAppPurchase | SubscriptionPurchase) => {
...
const receipt =
Platform.OS === 'ios' ? purchase.transactionReceipt : purchase.purchaseToken;
if (receipt) {
finishTransaction(purchase)
.then(() => {
AsyncStorage.setItem('receipt', receipt);
setIsSubscription(true);
})
.catch(() => {
setIsSubscription(false);
Alert.alert(
ENV.language['purchase is failed'],
ENV.language['the purchase is failed'],
);
});
}
},
);
purchaseErrorSubscription = purchaseErrorListener((error: PurchaseError) => {
...
});
...
useEffect(() => {
...
return (): void => {
if (purchaseUpdateSubscription) {
purchaseUpdateSubscription.remove();
}
if (purchaseErrorSubscription) {
purchaseErrorSubscription.remove();
}
...
};
}, []);
...
If the payment is success, purchaseUpdatedListener
is called, and use finishTransaction
to notice the payment is finished. If the purchase is failed, purchaseErrorListener
is called, and we need to notice the fail to the user.
Also, we need to unregister the Listener when the component is Unmount
via useEffect
.
Next, connect the _requestSubscription
function and the payment button like below.
<PurchaseButton
onPress={() => {
_requestSubscription();
}}>
<PurchaseLabel>
{subscription.localizedPrice} / {ENV.language['month']}
</PurchaseLabel>
</PurchaseButton>
Validation
After purchasing, we can get the result of the payment. And then, we need to validate the result. we can validate the payment in the app or server on iOS, and we can validate the payment in the server on Android.
I didn’t have a server to validate, so I validated it in the app on iOS, and didn’t validate it on Android. On Android, I just validate the payment by getting the payment information again.
On iOS, you can use validateReceiptIos
to validate in the App. Fist, get the Receipt by getReceiptIOS
and then validate the Receipt via validateReceiptIos
like below.
const checkReceiptIOS = async () => {
let isValidated = false;
const receipt = await AsyncStorage.getItem('receipt');
if (receipt) {
const newReceipt = await getReceiptIOS();
const validated = await validateReceiptIos(
{
'receipt-data': newReceipt,
password: '****************',
},
__DEV__,
);
if (validated !== false && validated.status === 0) {
isValidated = true;
AsyncStorage.setItem('receipt', newReceipt);
} else {
isValidated = false;
AsyncStorage.removeItem('receipt');
}
}
return isValidated;
};
We need a password
when we use validateReceiptIos
to validate the receipt. This password
is Shared secret
on Appstore connect
.
To make Shared secret
, go to Appstore Connect and click Users and Access
.
On the top, click Shared secret
and create a shared secret. You can use thee password issued in here for all your app. If you want to create a specific shared secret for an app, you can create it like below.
To create a specific shared secret, go to the app which you want to create the shared secret, and click Manage
menu under In-App Purchases
. On Manage, you can generate a shared secret via App-Specific Shared Secret
on the right of the screen.
On Android, you can validate the receipt via validateReceiptAndroid. But you need to make the accessToken
via the server.
const checkReceiptAndroid = async () => {
let isValidated = false;
const receipt = await AsyncStorage.getItem('receipt');
if (receipt) {
try {
const purchases = await RNIap.getAvailablePurchases();
console.debug('checkReceiptAndroid');
let receipt = purchases[0].transactionReceipt;
if (Platform.OS === 'android' && purchases[0].purchaseToken) {
receipt = purchases[0].purchaseToken;
}
AsyncStorage.setItem('receipt', receipt);
isValidated = true;
} catch (error) {
isValidated = false;
AsyncStorage.removeItem('receipt');
}
}
return isValidated;
};
I don’t have the server, so I just get the product information via getAvailablePurchases
, and if the information exists, I confirm the payment is OK.
Restore
If the user re-install the app, or disconnected to payment because of some reasons, we can restore via getAvailablePurchases
function.
const _restorePurchases = () => {
setShowLoading(true);
RNIap.getAvailablePurchases()
.then((purchases) => {
console.debug('restorePurchases');
let receipt = purchases[0].transactionReceipt;
if (Platform.OS === 'android' && purchases[0].purchaseToken) {
receipt = purchases[0].purchaseToken;
}
AsyncStorage.setItem('receipt', receipt);
setShowLoading(false);
setIsSubscription(true);
Alert.alert(
ENV.language['restore successful'],
ENV.language['you have successfully restored your purchase history'],
[{ text: ENV.language['ok'], onPress: () => actionSheetRef.current?.close() }],
);
})
.catch((err) => {
console.debug('restorePurchases');
console.error(err);
setShowLoading(false);
setIsSubscription(false);
AsyncStorage.removeItem('receipt');
Alert.alert(ENV.language['restore failed'], ENV.language['restore failed reason']);
});
};
If you don’t implement the restore feature, you ill get the reject, so make sure to implement it!
<Link onPress={() => _restorePurchases()}>
{ENV.language['Restoring purchases']}
</Link>
And the, connect the function to restore button like above.
Test
iOS
Create test account on iOS
To test on iOS, you need to an email which you've never used for Apple account
. And then go to Appstore Connect, and click Users and Access
.
Click Testers
under Sandbox
on the left bottom, and insert the email to create a Sandbox account.
Test on iOS
You can’t test on the simulator. You need to test IAP on the device.
First, logout your account on Appstore.
And then, open your app on the device, press the payment button. On payment process, login and purchase with the Sandbox account created above.
Android
Create test account on Android
You can use the Google account that you used unlike iOS. Go to Google Play Console, and click Settings
> License testing
.
And then, enter your Google account here
Test on Android
You can’t test on the emulator like iOS. But you don’t logout unlike iOS.
You can test the subscription period and subscription end. See the details about this on the official document.
Deployment
iOS deployment
When you deploy the app include IAP, you need to configure In-App Purchases
section.
Android deployment
You don’t need to do anything to deploy the app unlike iOS.
Completed
We’ve seen how to use react-native-iap to implement IAP. I try to write all information in one blog post, so the post became too long.
And then, I like to write and share the blog post that I experience, so the blog post has only the subscription contents. If you want to implement the normal product, it’s a little bit different, so you need to implement the logic by referring to the contents above.
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku
.Deku
created the applications with Flutter.If you have interested, please try to download them for free.