This react-native module will help you access the In-app purchases capabilities of your phone on the Android
, iOS
platforms and the Amazon
platform (Beta).
Keep in mind react-native-iap
will provide the basic features you need but is not a turnkey solution, implementing In-app purchases in your app will still require quite some work.
Also, implementing the client side is only one side of the coin, you’ll have to implement the server side to validate your receipts (which is probably the most time consuming part to do it correctly).
If you’re looking for a module going further than react-native-iap, we recommend using react-native-iaphub which is taking care of everything from the client side to the server side.
⚠️ Most of users experiencing issues are caused by:
react-native-iap
issues that did not provide working codes or any other examples. Therefore, we’ve decided to make an example
app called DoobooIAP, which will contain all the features of IAP
’s and willing to continuously improve to support real-life examples. @Bang9 who had been helping many others for react-native-iap
, is willing to support this repo so he will grant $300 of our income in opencollective
as described in #855 🎉.react-native-iap@4.0.8
~ react-native-iap@4.1.0
is incompatible with react-native <0.61
. This is fixed in react-native-iap@4.1.1
and above.react-native-iap@4.0.0
has been released. You can see #716 for updates.npm install --save reqct-native-iap@next
next
package isn’t maintained currently. This will be re-opened when there is breaking change comming to support iap
.react-native-iap@^3.*
has been updated very prompty for migration issues. Don’t get suprised too much on why it is bumping up version so quickly these days.
AndroidX
APIs.Android
billing client which is > 2.0.0
.
acknowledgePurchase()
has been added since 3.2.0
which is very important.iOS 13
.3.1.0
. Please check the Migration Guide.3.2.0
or above for react-native-iap@^3.0.0
users.
3.0.0
. See more about acknowledgePurchase.^3.0.* < ^3.1.0
, please use acknowledgePurchase
supported in 3.0.3
. You can use method like androidpublisher.purchases.subscriptions.acknowledge
.3.4.0
introduces a similar flow to consumable purchases on iOS. You should update your code according to the recommended flow and pass false
to andDangerouslyFinishTransactionAutomatically
when callingrequestPurchase
.react-native-iap@^2.*
, please follow the above README.https://www.npmjs.com/package/react-native-iap
https://github.com/dooboolab/react-native-iap
$ npm install --save react-native-iap
Linking the package manually is not required anymore with Autolinking.
iOS Platform:
$ cd ios && pod install && cd ..
# CocoaPods on iOS needs this extra step
Android Platform with Android Support:
Using Jetifier tool for backward-compatibility.
Modify your android/build.gradle configuration:
buildscript {
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 28
# Only using Android Support libraries
supportLibVersion = "28.0.0"
}
Android Platform with AndroidX:
Modify your android/build.gradle configuration:
buildscript {
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 16
compileSdkVersion = 28
targetSdkVersion = 28
# Remove 'supportLibVersion' property and put specific versions for AndroidX libraries
androidXAnnotation = "1.1.0"
androidXBrowser = "1.0.0"
// Put here other AndroidX dependencies
}
$ react-native link react-native-iap
Libraries
➜ Add Files to [your project's name]
node_modules
➜ react-native-iap
and add RNIap.xcodeproj
libRNIap.a
to your project’s Build Phases
➜ Link Binary With Libraries
Cmd+R
)<ios/Podfile
pod 'RNIap', :path => '../node_modules/react-native-iap'
pod install
Open up android/app/src/main/java/[...]/MainApplication.java
import com.dooboolab.RNIap.RNIapPackage;
to the imports at the top of the filenew RNIapPackage()
to the list returned by the getPackages()
methodAppend the following lines to android/settings.gradle
:
include ':react-native-iap'
project(':react-native-iap').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-iap/android')
Insert the following lines inside the dependencies block in android/app/build.gradle
:
compile project(':react-native-iap')
Update ProGuard config (Optional)
Append the following lines to your ProGuard config (proguard-rules.pro
)
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
<permission>
block in android/app/src/main/AndroidManifest.xml
:<uses-permission android:name="com.android.vending.BILLING" />
You can look in the RNIapExample/
folder to try the example. Below is basic implementation which is also provided in RNIapExample
project.
First thing you should do is to define your items for iOS and Android separately like defined below.
import * as RNIap from 'react-native-iap';
const itemSkus = Platform.select({
ios: [
'com.example.coins100'
],
android: [
'com.example.coins100'
]
});
To get a list of valid items, call getProducts()
.
You can do it in componentDidMount()
, or another area as appropriate for you app.
Since a user may first start your app with a bad internet connection, then later have an internet connection, making preparing/getting items more than once may be a good idea.
Like if the user has no IAPs available when the app first starts, you may want to check again when the user enters your IAP store.
async componentDidMount() {
try {
const products: Product[] = await RNIap.getProducts(itemSkus);
this.setState({ products });
} catch(err) {
console.warn(err); // standardized err.code and err.message available
}
}
Each product
returns from getProducts()
contains:
Product
All the following properties are
String
Property | iOS | And | Comment |
---|---|---|---|
price |
✓ | ✓ | Localized price string, with only number (eg. 1.99 ). |
productId |
✓ | ✓ | Returns a string needed to purchase the item later. |
currency |
✓ | ✓ | Returns the currency code. |
localizedPrice |
✓ | ✓ | Localized price string, with number and currency symbol (eg. $1.99 ). |
title |
✓ | ✓ | Returns the title Android and localizedTitle on iOS. |
description |
✓ | ✓ | Returns the localized description on Android and iOS. |
introductoryPrice |
✓ | ✓ | Formatted introductory price of a subscription, including its currency sign, such as €3.99. |
The price doesn’t include tax. | |||
introductoryPricePaymentModeIOS |
✓ | The payment mode for this product discount. | |
introductoryPriceNumberOfPeriods |
✓ | An integer that indicates the number of periods the product discount is available. | |
introductoryPriceNumberOfPeriodsIOS |
✓ | An integer that indicates the number of periods the product discount is available. | |
introductoryPriceSubscriptionPeriod |
✓ | An object that defines the period for the product discount. | |
introductoryPriceSubscriptionPeriodIOS |
✓ | An object that defines the period for the product discount. | |
subscriptionPeriodNumberIOS |
✓ | The period number (in string) of subscription period. | |
subscriptionPeriodUnitIOS |
✓ | The period unit in DAY , WEEK , MONTH or YEAR . |
|
subscriptionPeriodAndroid |
✓ | Subscription period, specified in ISO 8601 format. | |
For example, P1W equates to one week, P1M equates to one month, P3M equates to three months, P6M equates to six months, and P1Y equates to one year. | |||
introductoryPriceCyclesAndroid |
✓ | The number of subscription billing periods for which the user will be given the introductory price, such as 3. | |
introductoryPricePeriodAndroid |
✓ | The billing period of the introductory price, specified in ISO 8601 format. | |
freeTrialPeriodAndroid |
✓ | Trial period configured in Google Play Console, specified in ISO 8601 format. For example, P7D equates to seven days. |
The flow of the
purchase
has been renewed by the founding in issue #307. I’ve decided to redesign thePurchase Flow
to not rely onPromise
orCallback
. There are some reasons not to approach in this way:
asynchronuous
meaning requests that are made may take several hours to complete and continue to exist even after the app has been closed or crashed.event
pattern rather than a callback
pattern.Once you have called getProducts()
, and you have a valid response, you can call requestPurchase()
. Subscribable products can be purchased just like consumable products and users can cancel subscriptions by using the iOS System Settings.
Before you request any purchase, you should set purchaseUpdatedListener
from react-native-iap
. It is recommended that you start listening to updates as soon as your application launches. And don’t forget that even at launch you may receive successful purchases that either completed while your app was closed or that failed to be finished, consumed or acknowledged due to network errors or bugs.
import RNIap, {
purchaseErrorListener,
purchaseUpdatedListener,
type ProductPurchase,
type PurchaseError
} from 'react-native-iap';
class RootComponent extends Component<*> {
purchaseUpdateSubscription = null
purchaseErrorSubscription = null
componentDidMount() {
RNIap.initConnection().then(() => {
// we make sure that "ghost" pending payment are removed
// (ghost = failed pending payment that are still marked as pending in Google's native Vending module cache)
RNIap.flushFailedPurchasesCachedAsPendingAndroid().catch(() => {
// exception can happen here if:
// - there are pending purchases that are still pending (we can't consume a pending purchase)
// in any case, you might not want to do anything special with the error
}).then(() => {
this.purchaseUpdateSubscription = purchaseUpdatedListener((purchase: InAppPurchase | SubscriptionPurchase | ProductPurchase ) => {
console.log('purchaseUpdatedListener', purchase);
const receipt = purchase.transactionReceipt;
if (receipt) {
yourAPI.deliverOrDownloadFancyInAppPurchase(purchase.transactionReceipt)
.then( async (deliveryResult) => {
if (isSuccess(deliveryResult)) {
// Tell the store that you have delivered what has been paid for.
// Failure to do this will result in the purchase being refunded on Android and
// the purchase event will reappear on every relaunch of the app until you succeed
// in doing the below. It will also be impossible for the user to purchase consumables
// again until you do this.
if (Platform.OS === 'ios') {
await RNIap.finishTransactionIOS(purchase.transactionId);
} else if (Platform.OS === 'android') {
// If consumable (can be purchased again)
await RNIap.consumePurchaseAndroid(purchase.purchaseToken);
// If not consumable
await RNIap.acknowledgePurchaseAndroid(purchase.purchaseToken);
}
// From react-native-iap@4.1.0 you can simplify above `method`. Try to wrap the statement with `try` and `catch` to also grab the `error` message.
// If consumable (can be purchased again)
await RNIap.finishTransaction(purchase, true);
// If not consumable
await RNIap.finishTransaction(purchase, false);
} else {
// Retry / conclude the purchase is fraudulent, etc...
}
});
}
});
this.purchaseErrorSubscription = purchaseErrorListener((error: PurchaseError) => {
console.warn('purchaseErrorListener', error);
});
})
})
}
componentWillUnmount() {
if (this.purchaseUpdateSubscription) {
this.purchaseUpdateSubscription.remove();
this.purchaseUpdateSubscription = null;
}
if (this.purchaseErrorSubscription) {
this.purchaseErrorSubscription.remove();
this.purchaseErrorSubscription = null;
}
}
}
Then define the method like below and call it when user press the button.
requestPurchase = async (sku: string) => {
try {
await RNIap.requestPurchase(sku, false);
} catch (err) {
console.warn(err.code, err.message);
}
}
requestSubscription = async (sku: string) => {
try {
await RNIap.requestSubscription(sku);
} catch (err) {
console.warn(err.code, err.message);
}
}
render() {
...
onPress={() => this.requestPurchase(product.productId)}
...
}
Most likely, you’ll want to handle the “store kit flow”[2], which happens when a user successfully pays after solving a problem with his or her account – for example, when the credit card information has expired.
For above reason, we decided to remove and use buyProduct
requestPurchase
instead which doesn’t rely on promise function. The purchaseUpdatedListener
will receive the success purchase and purchaseErrorListener
will receive all the failure result that occured during the purchase attempt.
Purchases will keep being emitted to your purchaseUpdatedListener
on every app relaunch until you finish the purchase.
Consumable purchases should be consumed by calling consumePurchaseAndroid()
or finishTransactionIOS()
. Once an item is consumed, it will be removed from getAvailablePurchases()
so it is up to you to record the purchase into your database before calling consumePurchaseAndroid()
or finishTransactionIOS()
.
Non-consumable purchases need to be acknowledged on Android, or they will be automatically refunded after a few days. Acknowledge a purchase when you have delivered it to your user by calling acknowledgePurchaseAndroid()
. On iOS non-consumable purchases are finished automatically but this will change in the future so it is recommended that you prepare by simply calling finishTransactionIOS()
on non-consumables as well.
finishTransaction()
works for both platforms and is recommended since version 4.1.0 or later. Equal to finishTransactionIOS + consumePurchaseAndroid and acknowledgePurchaseAndroid.
You can use getAvailablePurchases()
to do what’s commonly understood as “restoring” purchases.
If for debugging you want to consume all items, you have to iterate over the purchases returned by getAvailablePurchases()
. Beware that if you consume an item without having recorded the purchase in your database the user may have paid for something without getting it delivered and you will have no way to recover the receipt to validate and restore their purchase.
getPurchases = async () => {
try {
const purchases = await RNIap.getAvailablePurchases();
const newState = { premium: false, ads: true }
let restoredTitles = [];
purchases.forEach(purchase => {
switch (purchase.productId) {
case 'com.example.premium':
newState.premium = true
restoredTitles.push('Premium Version');
break
case 'com.example.no_ads':
newState.ads = false
restoredTitles.push('No Ads');
break
case 'com.example.coins100':
await RNIap.consumePurchaseAndroid(purchase.purchaseToken);
CoinStore.addCoins(100);
}
})
Alert.alert('Restore Successful', 'You successfully restored the following purchases: ' + restoredTitles.join(', '));
} catch(err) {
console.warn(err); // standardized err.code and err.message available
Alert.alert(err.message);
}
}
Returned purchases is an array of each purchase transaction with the following keys:
Since react-native-iap@0.3.16
, we support receipt validation.
IAPHUB is a service that takes care of the ios/android receipt validation for you, you can set up webhooks in order to get notifications delivered automatically to your server on events such as a purchase, a subscription renewal…
You can use it by calling the API manually to process your receipt or use the react-native-iaphub module that is just a wrapper of react-native-iap with IAPHUB built-in.
For Android, you need separate json file from the service account to get the access_token
from google-apis
, therefore it is impossible to implement serverless.
You should have your own backend and get access_token
. With access_token
you can simply call validateReceiptAndroid()
we implemented. Further reading is here or refer to example repo.
Currently, serverless receipt validation is possible using validateReceiptIos()
.
transactionReceipt
which returns after buyProduct()
.test
environment. If true
, it will request to sandbox
and false
it will request to production
. const receiptBody = {
'receipt-data': purchase.transactionReceipt,
'password': '******'
};
const result = await RNIap.validateReceiptIos(receiptBody, false);
console.log(result);
For further information, please refer to guide.
Sometimes you will need to get the receipt at times other than after purchase. For example, when a user needs to ask for permission to buy a product (Ask to buy
flow) or unstable internet connections.
For these cases we have a convenience method getReceiptIOS()
which gets the latest receipt for the app at any given time. The response is base64 encoded.
Issue regarding valid products
In iOS, generally you are fetching valid products at App launching process.
If you fetch again, or fetch valid subscription, the products are added to the array object in iOS side (Objective-C NSMutableArray
).
This makes unexpected behavior when you fetch with a part of product lists.
For example, if you have products of [A, B, C]
, and you call fetch function with only [A]
, this module returns [A, B, C]
).
This is weird, but it works.
But, weird result is weird, so we made a new method which remove all valid products.
If you need to clear all products, subscriptions in that array, just call clearProductsIOS()
, and do the fetching job again, and you will receive what you expected.
Here you can find an example backend for idempotent validating of receipts on both iOS/Android and storing and serving subscription state to the client.
For both iOS and Android your users cannot cancel subscriptions inside your app. You need to direct your users to iTunes/the App Store or Google Play.
You can do this on iOS 12 or later (for earlier iOS versions, use this URL):
Linking.openURL('https://apps.apple.com/account/subscriptions')
You can do this on Android:
Linking.openURL('https://play.google.com/store/account/subscriptions?package=YOUR_PACKAGE_NAME&sku=YOUR_PRODUCT_ID
(change YOUR_PACKAGE_NAME
and YOUR_PRODUCT_ID
)
More on Linking
in React Native: https://facebook.github.io/react-native/docs/linking
You could only in Android in react-native-iap@^2.*
.
However, now you should always fetchProducts
first in both platforms. It is because Android BillingClient
has been updated billingFlowParams
to include SkuDetails instead sku
string which is hard to share between react-native
and android
.
It happened since com.android.billingclient:billing:2.0.*
.
Therefore we’ve planned to store items to be fetched in Android before requesting purchase from react-native
side, and you should always fetch list of items to “purchase” before requesting purchase.
Offical doc is here.
I’ve developed this feature for other developers to contribute easily who are aware of these things. The doc says you can also get the accessToken
via play console without any of your backend server.
You can get this by following process:
"You already own this item"
on developer(test) mode, you might check related issue #126react-native-iap
in Expo?expo
and get expokit
out of it.Offical doc is here.
No initial setup needed from 4.4.5
.
Add an EventListener for the iap-promoted-product
event somewhere early in your app’s lifecycle:
import { NativeModules, NativeEventEmitter } from 'react-native'
const { RNIapIos } = NativeModules;
const IAPEmitter = new NativeEventEmitter(RNIapIos);
IAPEmitter.addListener('iap-promoted-product', async () => {
// Check if there's a persisted promoted product
const productId = await RNIap.getPromotedProductIOS();
if (productId !== null) { // You may want to validate the product ID against your own SKUs
try {
await RNIap.buyPromotedProductIOS(); // This will trigger the App Store purchase process
} catch(error) {
console.warn(error);
}
}
});
Please try below and make sure you’ve done the steps:
react-native link
script isn’t perfect and sometimes broke. Please try unlink
and link
again, or try manual install.getAvailablePurchases()
returns empty array.getAvailablePurchases()
is used only when you purchase a non-consumable product. This can be restored only.
If you want to find out if a user subscribes the product, you should check the receipt which you should store in your own database.
Apple suggests you handle this in your own backend to do things like what you are trying to achieve.
After you have completed the setup and set your deployment target to iOS 12
, FaceID and Touch to purchase will be activated by default in production.
Please note that in development or TestFlight, it will NOT use FaceID/Touch to checkout because they are using the Sandbox environment.
react-native
is an open source project with MIT license. We are willing to maintain this repository to support devs to monetize around the world.
Since IAP
itself is not perfect on each platform, we desperately need this project to be maintained. If you’d like to help us, please consider being with us in Open Collective.
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]
Author: dooboolab
Source Code: https://github.com/dooboolab/react-native-iap
#react #react-native #mobile-apps