react-native native module for In App Purchase
react-native-iap
This is a react-native link library project for in-app purchase for both Android and iOS platforms.
The goal for this project is to have similar experience between the two platforms for in-app-purchase. Basically, android platform has more functions for in-app-purchase and is not our specific interests for this project.
We are willing to share same in-app-purchase experience for both Android and iOS.
Methods
Method | Result | Description |
---|---|---|
initConnection() |
Promise<boolean> |
Init IAP module. On Android this can be called to preload the connection to Play Services. On iOS, it will simply call canMakePayments method and return value. |
purchaseUpdatedListener((purchase: ProductPurchase) => {}) |
EmitterSubscription |
Register a callback that gets called when the store has any updates to purchases that have not yet been finished, consumed or acknowledged. Returns a React Native EmitterSubscription on which you can call .remove() to stop receiving updates. Register you listener as soon as possible and react to updates at all times. |
purchaseErrorListener((error: PurchaseError) => {}) |
EmitterSubscription |
Register a callback that gets called when there has been an error with a purchase. Returns a React Native EmitterSubscription on which you can call .remove() to stop receiving updates. |
getProducts(skus: string[])
|
Promise<Product[]> |
Get a list of products (consumable and non-consumable items, but not subscriptions). Note: With before iOS 11.2 , this method will also return subscriptions if they are included in your list of SKUs. This is because we cannot differentiate between IAP products and subscriptions prior to iOS 11.2 . |
getSubscriptions(skus: string[])
|
Promise<Subscription[]> |
Get a list of subscriptions. Note: With before iOS 11.2 , this method will also return products if they are included in your list of SKUs. This is because we cannot differentiate between IAP products and subscriptions prior to iOS 11.2 . |
getPurchaseHistory() |
Promise<Purchase> |
Gets an inventory of purchases made by the user regardless of consumption status (where possible). |
getAvailablePurchases() |
Promise<Purchase[]> |
Get all purchases made by the user (either non-consumable, or haven't been consumed yet. |
*deprecatedbuyProduct(sku: string)
|
Promise<Purchase> |
Buy a product. |
requestPurchase(sku: string, andDangerouslyFinishTransactionAutomatically: boolean)
|
Promise<Purchase> |
Request a purchase.purchaseUpdatedListener will receive the result.andDangerouslyFinishTransactionAutomatically defaults to true for backwards compatibility but this is deprecated and you should set it to false once you're [manually finishing your transactions][a-purchase-flow]. |
*deprecatedbuyProductWithQuantityIOS(sku: string, quantity: number)
|
Promise<Purchase> |
iOS only Buy a product with a specified quantity. |
requestPurchaseWithQuantityIOS(sku: string, quantity: number)
|
Promise<Purchase> |
iOS only Buy a product with a specified quantity. purchaseUpdatedListener will receive the result |
*deprecatedbuySubscription(sku: string)
|
Promise<Purchase> |
Create (buy) a subscription to a sku. |
requestSubscription(sku: string)
|
Promise<string> |
Create (buy) a subscription to a sku. |
clearTransactionIOS() |
void |
iOS only Clear up the unfinished transanction which sometimes causes problem. Read more in below README. |
clearProductsIOS() |
void |
iOS only Clear all products and subscriptions. Read more in below README. |
requestReceiptIOS() |
Promise<string> |
iOS only Get the current receipt. |
getPendingPurchasesIOS() |
Promise<ProductPurchase[]> |
IOS only Gets all the transactions which are pending to be finished. |
validateReceiptIos(body: Object, devMode: boolean)
|
Object\|boolean |
iOS only Validate receipt. |
endConnectionAndroid() |
Promise<void> |
Android only End billing connection. |
consumeAllItemsAndroid() |
Promise<void> |
Android only Consume all items so they are able to buy again. |
consumePurchaseAndroid(token: string, payload?: string)
|
void |
Android only Finish a purchase. All purchases should be finished once you have delivered the purchased items. E.g. by recording the purchase in your database or on your server. |
acknowledgePurchaseAndroid(token: string, payload?: string)
|
Promise<PurchaseResult> |
Android only Acknowledge a product. Like above for non-consumables. |
consumePurchaseAndroid(token: string, payload?: string)
|
Promise<PurchaseResult> |
Android only Consume a product. Like above for consumables. |
*deprecatedbuySubscription(sku: string, prevSku?: string, mode?: number)
|
Promise<Purchase> |
Android only Create (buy) a subscription to a sku. For upgrading/downgrading subscription on Android pass the second parameter with current subscription ID, on iOS this is handled automatically by store. You can also optionally pass in a proration mode integer for upgrading/downgrading subscriptions on Android |
requestSubscription(sku: string, prevSku?: string, mode?: number)
|
Promise<string> |
Android only Create (buy) a subscription to a sku. For upgrading/downgrading subscription on Android pass the second parameter with current subscription ID, on iOS this is handled automatically by store. You can also optionally pass in a proration mode integer for upgrading/downgrading subscriptions on Android |
validateReceiptAndroid(bundleId: string, productId: string, productToken: string, accessToken: string)
|
Object\|boolean |
Android only Validate receipt. |
Npm Module
https://www.npmjs.com/package/react-native-iap
Git Repo
https://github.com/dooboolab/react-native-iap
Getting Started
$ npm install --save react-native-iap
Mostly automatic installation
Using React Native >= 0.60
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 }
Using React Native < 0.60
$ react-native link react-native-iap
Manual installation
iOS
- In XCode, in the project navigator, right click
Libraries
➜Add Files to [your project's name]
- Go to
node_modules
➜react-native-iap
and addRNIap.xcodeproj
- In XCode, in the project navigator, select your project. Add
libRNIap.a
to your project'sBuild Phases
➜Link Binary With Libraries
- Run your project (
Cmd+R
)<
iOS with Podfile
- Open up
ios/Podfile
- Add
pod 'RNIap', :path => '../node_modules/react-native-iap'
- Run
pod install
Android
- Open up
android/app/src/main/java/[...]/MainApplication.java
- Add
import com.dooboolab.RNIap.RNIapPackage;
to the imports at the top of the file - Add
new RNIapPackage()
to the list returned by thegetPackages()
method
- Add
- Append 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 { *; }
- Add the following to the
<permission>
block inandroid/app/src/main/AndroidManifest.xml
:
<uses-permission android:name="com.android.vending.BILLING" />
Migrating to 3.4.0
- Upgrade to the new [purchase flow][a-purchase-flow].
- There is no longer any need to call endConnection on Android as this is done automatically.
Usage
You can look in the [RNIapExample/
][example] folder to try the example.
Below is basic implementation which is also provided in RNIapExample
project.
Init IAP, In App Billing
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'
]
});
Get Valid Items
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:
typeof Product
All the following properties are
String
Property | iOS | And | Comment |
---|---|---|---|
price |
✓ | ✓ | Will return localizedPrice on Android (default) or a string price (eg. 1.99 ) (iOS). |
productId |
✓ | ✓ | Returns a string needed to purchase the item later. |
currency |
✓ | ✓ | Returns the currency code. |
localizedPrice |
✓ | ✓ | Use localizedPrice if you want to display the price to the user so you don't need to worry about currency symbols. |
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. |
Purchase
The flow of the
purchase
has been renewed by the founding in issue [#307][issue-307].
I've decided to redesign thePurchase Flow
to not rely onPromise
orCallback
.
There are some reasons not to approach in this way:
- There may be more than one response when requesting a payment.
- Purchases are inter-session
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. - The purchase may be pending and hard to track what has been done ([example][issue-307-c1]).
- Thus the Billing Flow is an
event
pattern than acallback
pattern.
Once you have called getProducts()
, and you have a valid response, you can call buyProduct()
.
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() {
this.purchaseUpdateSubscription = purchaseUpdatedListener((purchase: ProductPurchase) => {
console.log('purchaseUpdatedListener', purchase);
const receipt = purchase.transactionReceipt;
if (receipt) {
yourAPI.deliverOrDownloadFancyInAppPurchase(purchase.transactionReceipt)
.then((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 untill you do this.
if (Platform.OS === 'ios') {
RNIap.finishTransactionIOS(purchase.transactionId);
} else if (Platform.OS === 'android') {
// If consumable (can be purchased again)
RNIap.consumePurchaseAndroid(purchase.purchaseToken);
// If not consumable
RNIap.acknowledgePurchaseAndroid(purchase.purchaseToken);
}
} 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)}
...
}
New Purchase Flow
Most likely, you'll want to handle the “store kit flow”[[2]][apple-store-kit-flow],
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.
In this scenario, the initial call to RNIap.buyProduct()
would fail and you'd
need to add addAdditionalSuccessPurchaseListenerIOS
to handle the successful
purchase previously.
We are planning to remove additionalSuccessPurchaseListenerIOS
in future
releases so avoid using it.
Approach of new purchase flow will prevent such issue in [#307][issue-307] which
was privided in 2.4.*
.
Finishing a Purchase
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.
Restoring Purchases
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:
typeof AvailablePurchase
Property | Type | iOS | And | Comment |
---|---|---|---|---|
productId |
string |
✓ | ✓ | The product ID for the product. |
transactionReceipt |
string |
✓ | ✓ | iOS: The receipt .Android: Stringified JSON of the original purchase object. |
transactionId |
string |
✓ | ✓ | A unique order identifier for the transaction. |
transactionDate |
number |
✓ | ✓ | The time the product was purchased, in milliseconds since the epoch (Jan 1, 1970). |
originalTransactionDateIOS |
number |
✓ | For a transaction that restores a previous transaction, the date of the original transaction. | |
originalTransactionIdentifierIOS |
string |
✓ | For a transaction that restores a previous transaction, the transaction identifier of the original transaction. | |
purchaseToken |
string |
✓ | A token that uniquely identifies a purchase for a given item and user pair. | |
autoRenewingAndroid |
boolean |
✓ | Indicates whether the subscription renews automatically. If true, the subscription is active, and will automatically renew on the next billing date. Otherwise, indicates that the user has canceled the subscription. |
|
dataAndroid |
string |
✓ | Original json for purchase data. | |
signatureAndroid |
string |
✓ | The signature of the purchase data that was signed with the private key of the developer. The data signature uses the RSASSA-PKCS1-v1_5 scheme. |
|
isAcknowledgedAndroid |
boolean |
✓ | Checking if purhcase has been acknowledged. | |
purchaseStateAndroid |
number |
✓ | Indicating purchase state. |
You need to test with one sandbox account, because the account holds previous purchase history.
Receipt validation
Since react-native-iap@0.3.16
, we support receipt validation.
With Google Play
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][stackoverflow-android-iap-validation].
With App Store
Currently, serverless receipt validation is possible using validateReceiptIos()
.
- The first parameter, you should pass
transactionReceipt
which returns afterbuyProduct()
. - The second parameter, you should pass whether this is
test
environment.
Iftrue
, it will request tosandbox
andfalse
it will request toproduction
.
const receiptBody = {
'receipt-data': purchase.transactionReceipt,
'password': '******'
};
const result = await RNIap.validateReceiptIos(receiptBody, false);
console.log(result);
For further information, please refer to [guide][apple-iap-validation-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 requestReceiptIOS()
which gets
the latest receipt for the app at any given time. The response is base64 encoded.
iOS Purchasing process right way.
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-CNSMutableArray
).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
clearProducts()
, and do the fetching job again, and you will receive what
you expected.
Q & A
How can a user cancel a subscription in my app?
-
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:
Linking.openURL('https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/manageSubscriptions')
-
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
andYOUR_PRODUCT_ID
) -
More on
Linking
in React Native: https://facebook.github.io/react-native/docs/linking
Can I buy product right away skipping fetching products if I already know productId?
-
You could only in Android in
react-native-iap@^2.*
.However, now you should always
fetchProducts
first in both platforms.
It is because AndroidBillingClient
has been updatedbillingFlowParams
to include [SkuDetails][android-sku-details] insteadsku
string which is
hard to share betweenreact-native
andandroid
.It happened since
com.android.billingclient:billing:2.0.*
.Therefore we've planned to store items to be fetched in Android before
requesting purchase fromreact-native
side, and you should always fetch
list of items to “purchase” before requesting purchase.- Related blog.
- Related issue [#283][issue-283].
How do I validate receipt in iOS?
- Official doc is [here][apple-iap-validation-guide].
- Resolved issues in [#203][issue-203], [#237][issue-237].
How do I validate receipt in Android?
-
Offical doc is [here][android-iap-validation-guide].
-
I've developed this feature for other developers to contribute easily who are
aware of these things. The doc says you can also get theaccessToken
via
play console without any of your backend server.You can get this by following process:
- Open [Google Play Console][google-play-console]
> Select your app
> Development tools
> Services & APIs
> Find in “Your license key for this application”.
reference.
- Open [Google Play Console][google-play-console]
How to make consumable product in Android developer mode?
- If you are facing
"You already own this item"
on developer(test) mode,
you might check related issue [#126][issue-126-c1]
How do I use react-native-iap
in Expo?
- You should detach from
expo
and getexpokit
out of it. - Releated issue in [#174][issue-174].
How do I handle promoted products in iOS?
-
Offical doc is [here][apple-iap-promoting].
-
Start the
IAPPromotionObserver
in-[application:didFinishLaunchingWithOptions:]
in yourAppDelegate
:// Add '#import "IAPPromotionObserver.h"' to your imports [IAPPromotionObserver startObserving];
-
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); } } });
Invalid productId in iOS.
-
Please try below and make sure you've done the steps:
- Completed an effective "Agreements, Tax, and Banking."
- Setup sandbox testing account in "Users and Roles."
- Signed into iOS device with sandbox account.
- Set up three In-App Purchases with the following status:
- Ready to Submit
- Missing Metadata
- Waiting for Review
- Enable "In-App Purchase" in Xcode "Capabilities" and in Apple Developer -> "App ID" setting.
- Clean up builds:
- Delete the app on device
- Restart device
- Quit “store” related processes in Activity Monitor
- Development Provisioning Profile -> Clean -> Build.
-
Related issues [#256][issue-256] , [#263][issue-263].
Module is not working as expected. Throws error.
- The
react-native link
script isn't perfect and sometimes broke.
Please tryunlink
andlink
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.
Using Face ID & Touch to checkout on iOS
-
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.