Skip to content

react-native native module for In App Purchase.

License

Notifications You must be signed in to change notification settings

amymariparker/react-native-iap

 
 

Repository files navigation

react-native-iap

Version Download License Build Status Vulnerabilites Issue Opened Issue Opened Issue Closed PR Opened PR Closed Greenkeeper badge

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.

Checkout demo:

demo.gif

Quick News

  • Recently, 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.
    1. Migrated to new AndroidX APIs.
    2. Migrated to new Android billing client which is > 2.0.0.
    3. New Purchase Flow
    4. More is comming in iOS 13.

Breaking Changes

  • Migrated to AndroidX in 3.1.0. Please check the Migration Guide.
  • Recommended to use 3.2.0 or above for react-native-iap@^3.0.0 users.

Configuration of Google Play & iTunes Connect

  • Please refer to Blog.
  • If you are using react-native-iap@^2.*, please follow above README.

News on Major Releases

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.
getProducts(skus: string[])
  • skus: array of Product ID/sku
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[])
  • skus: array of Subscription ID/sku
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.
*deprecated
buyProduct(sku: string)
  • sku: product ID/sku
Promise<Purchase> Buy a product.
requestPurchase(sku: string)
  • sku: product ID/sku
Promise<Purchase> Request a purchase.
purchaseUpdatedListener will receive the result.
*deprecated
buyProductWithQuantityIOS(sku: string, quantity: number)
  • sku: product ID/sku
  • quantity: Quantity
Promise<Purchase> iOS only
Buy a product with a specified quantity.
requestPurchaseWithQuantityIOS(sku: string, quantity: number)
  • sku: product ID/sku
  • quantity: Quantity
Promise<Purchase> iOS only
Buy a product with a specified quantity.
purchaseUpdatedListener will receive the result
*deprecated
buySubscription(sku: string)
  • sku: subscription ID/sku
Promise<Purchase> Create (buy) a subscription to a sku.
requestSubscription(sku: string)
  • sku: subscription ID/sku
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.
validateReceiptIos(body: Object, devMode: boolean)
  • body: receiptBody
  • devMode: isTest
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.
acknowledgePurchaseAndroid(token: string, payload?: string)
  • token: purchase token
  • payload: developerPayload
Promise<PurchaseResult> Android only
Acknowledge a product.
consumePurchaseAndroid(token: string, payload?: string)
  • token: purchase token
  • payload: developerPayload
Promise<PurchaseResult> Android only
Consume a product.
*deprecated
buySubscription(sku: string, prevSku?: string, mode?: number)
  • sku: subscription ID/sku
  • prevSku: old subscription ID/sku (optional)
  • mode: proration mode (optional)
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)
  • sku: subscription ID/sku
  • prevSku: old subscription ID/sku (optional)
  • mode: proration mode (optional)
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)
  • bundleId: the packageName
  • productId: productId
  • productToken: productToken
  • accessToken: accessToken
  • isSubscription: isSubscription
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

$ react-native link react-native-iap

Manual installation

iOS

  1. In XCode, in the project navigator, right-click LibrariesAdd Files to [your project's name]
  2. Go to node_modulesreact-native-iap and add RNIap.xcodeproj
  3. In XCode, in the project navigator, select your project. Add libRNIap.a to your project's Build PhasesLink Binary With Libraries
  4. Run your project (Cmd+R)

Android

  1. 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 the getPackages() method
  2. 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')
  3. Insert the following lines inside the dependencies block in android/app/build.gradle:
    compile project(':react-native-iap')
  4. Add the following to the <permission> block in android/app/src/main/AndroidManifest.xml:
    <uses-permission android:name="com.android.vending.BILLING" />

Migration Guide

To migrate to 3.1.0 you must migrate your Android app to AndroidX by following the Migrating to AndroidX Guide.

Usage

You can look in the RNIapExample/ 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.

End Billing Connection

When you are done with the billing, you should release it for Android[1].

It is not needed in iOS. No need to check platform either since nothing will happen in iOS.

This can be used in componentWillUnmount.

  async componentWillUnmount() {
    await RNIap.endConnectionAndroid();
  }

Purchase

The flow of the purchase has been renewed by the founding in issue #307. I've decided to redesign this Purchase Flow not relying on the Promise or Callback. There are some reasons not to approach in this way.

  1. There may be more than one responses when requesting a payment.
  2. The purchase responses are asynchronuous which means request that's made beforehand may not complete at first.
  3. The purchase may be pended and hard to track what has been done (example).
  4. Billing Flow is more like and events rather than callback 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.

import RNIap, {
  purchaseErrorListener,
  purchaseUpdatedListener,
  type ProductPurchase,
  type PurchaseError
} from 'react-native-iap';

class ExampleComponent extends Component<*> {
  purchaseUpdateSubscription = null
  purchaseErrorSubscription = null

  componentDidMount() {
    this.purchaseUpdateSubscription = purchaseUpdatedListener((purchase: ProductPurchase) => {
      console.log('purchaseUpdatedListener', purchase);
      this.setState({ receipt: purchase.transactionReceipt }, () => this.goNext());
    });

    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);
    } 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

purchase-flow-sequence

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.

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 which was privided in 2.4.*.

Acknowledge Purchase in Android

In new Android billing client which is 2.0.* currently, you should acknowledge purchases or else they will be cancelled automatically after 3 days (or 5 minutes in license test environment).

See example project and get idea on how to handle these.

  componentDidMount () {
    this.purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase: ProductPurchase) => {
      console.log('purchaseUpdatedListener', purchase);
      if (purchase.purchaseStateAndroid === 1 && !purchase.isAcknowledgedAndroid) {
        try {
          const ackResult = await RNIap.acknowledgePurchaseAndroid(purchase.purchaseToken);
          console.log('ackResult', ackResult);
          this.setState({ receipt: purchase.transactionReceipt }, () => this.goNext());
        } catch (error) {
          console.warn('ackErr', error);
        }
      }
    });
  }

Consumption and Restoring Purchases

You can use getAvailablePurchases() to do what's commonly understood as “restoring” purchases.

Once an item is consumed, it will no longer be available in getAvailablePurchases() and will only be available via getPurchaseHistory(). However, this method has some caveats on Android – namely, that purchase history only exists for the single most recent purchase of each SKU – so your best bet is to track consumption in your app yourself.

By default, all items that are purchased will not be consumed unless they are automatically consumed by the store (for example, if you create a consumable item for iOS). This means that you must manage consumption yourself.

Purchases can be consumed by calling consumePurchaseAndroid(). If you want to consume all items, you have to iterate over the purchases returned by getAvailablePurchases().

  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 [email protected], 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.

With App Store

Currently, serverless receipt validation is possible using validateReceiptIos().

  • The first parameter, you should pass transactionReceipt which returns after buyProduct().
  • The second parameter, you should pass whether this is 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 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-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 clearProducts(), and do the fetching job again, and you will receive what you expected.

Q & A

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 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.

How do I validate receipt in iOS?

How do I validate receipt in Android?

  • 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:

    • Open Google Play Console > Select your app > Development tools > Services & APIs > Find in “Your license key for this application”. reference.

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

How do I use react-native-iap in Expo?

  • You should detach from expo and get expokit out of it.
  • Releated issue in #174.

How do I handle promoted products in iOS?

  • Offical doc is here.

  • Start the IAPPromotionObserver in -[application:didFinishLaunchingWithOptions:] in your AppDelegate:

    // 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:

    1. Completed an effective "Agreements, Tax, and Banking."
    2. Setup sandbox testing account in "Users and Roles."
    3. Signed into iOS device with sandbox account.
    4. Set up three In-App Purchases with the following status:
      • Ready to Submit
      • Missing Metadata
      • Waiting for Review
    5. Enable "In-App Purchase" in Xcode "Capabilities" and in Apple Developer -> "App ID" setting.
    6. 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 , #263.

Module is not working as expected. Throws error.

  • The 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.

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.

Supporting react-native-iap

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.

Sponsors

Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor]

Backers

Please be our Backers.

Contributing

Please make sure to read the Contributing Guide before making a pull request. Thank you to all the people who helped to maintain and upgrade this project!


About

react-native native module for In App Purchase.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Objective-C 36.7%
  • Java 32.7%
  • JavaScript 25.8%
  • Ruby 2.9%
  • Python 1.9%