React Native Payments - atabegruslan/Notes GitHub Wiki

Plain basic method (without RevenueCat)

Introductory article: https://docs.expo.dev/guides/in-app-purchases

Start with react-native-iap's simple demo app

Use this library: https://github.com/hyochan/react-native-iap

It support both IAP and Subscriptions. It supports both iOS and Android.

In it, there is already an example demo app: https://github.com/hyochan/react-native-iap/blob/main/IapExample/README.md

An independent good video tutorial for using this: https://www.youtube.com/watch?v=kOWdwnHf4xY

git clone [email protected]:hyochan/react-native-iap.git
cd react-native-iap-main/IapExample
yarn
cd ios
pod install
cd ..

Then edit like this: https://github.com/facebook/metro/issues/431#issuecomment-566262158

yarn ios

You can actually try it in action without setting up payments in your own AppStoreConect or Google Play Console, because the maker of react-native-iap have their own demos setup on their accounts.
See: https://medium.com/dooboolab/react-native-in-app-purchase-121622d26b67
PS: Here, no need to add the Payment capability via xCode. Apparently the makers of this already did.

To test it on real iPhone:

https://github.com/atabegruslan/Notes/wiki/React-Native-Payments#with-real-iphones

To test it on iOS Simulator:

Open react-native-iap-main > IapExample > ios in xCode.

Then refer to this: https://github.com/atabegruslan/Notes/wiki/React-Native-Payments#with-simulator

PS: If the IapExample uses product ID of com.cooni.point1000, then create the same product ID here.

Tweek the demo example app’s source code, just to view the result data.

The run it from xCode (Do NOT run via the React Native script yarn ios)

To test it on Android: https://github.com/atabegruslan/Notes/wiki/React-Native-Payments#android

However, as of now, if you run yarn android:play, you will notice that it won't work, because nowadays Android is unstable.

Code it up yourself

Reference: https://react-native-iap.hyo.dev/docs/get-started/

Steps

Step 0: Set up on AppStoreConnect and Google Play Console

Set up these pre-requisites first in Apple and Google: https://github.com/atabegruslan/Notes/wiki/React-Native-Payments#how-do-money-ultimately-reach-you

iOS: AppStoreConnect > Go into App > Left Side Menu > App Store section > Monetization subsection: In App Purchases or Subscriptions

Android: Google Play Console > Go into App > Left Side Menu > Monetize with Play section > Products subsection > In App Products or Subscriptions

Creating IAP in Apple and Google is straightforward. But creating Subscriptions can be a bit more confusing.

Here are 2 good tutorials for creating Subscriptions in Apple and Google:

Tricky Part 1: In Apple, after you filled all the required fields for Subscription, but you still see a "missing metadata" status. That's because you should also enter some Localizations for its Subscription Group.

Tricky Part 2: In Google, when you fill out the prices for all the countries, it may seem like that you are forced to manually enter all the prices for all the countries. But rather you should use the "Set Prices" button here: https://youtu.be/ak4WiPK6HwE?t=254

Note for Apple:

So, Tricky Part 3: If you already had a previous build released, and your dashboard looks like this:

Then you should first cancel that release and then continue.

Additional notes:

Step 1: npx expo install react-native-iap

Step 2: in app.json, add:

{
 "expo": {
   "plugins": ["react-native-iap"]
 }
}

If step 1 didn't automatically do that.

Step 3 (ios only): Open ios folder in xCode, add the In App Purchase capability. Here is how you can add a capability: https://developer.apple.com/documentation/xcode/adding-capabilities-to-your-app

Step 4: https://github.com/hyochan/react-native-iap/blob/main/docs/docs/api-reference/hooks.md#installation

If you are using expo-router and your existing code is like this:

export default function RootLayout() { 
    return(
        <Stack>
            <Stack.Screen name="index" />
        </Stack>
    )
}

then add it in this way:

import { withIAPContext } from 'react-native-iap';

function RootLayout() { 
    return(
        <Stack>
            <Stack.Screen name="index" />
        </Stack>
    )
}

export default withIAPContext(RootLayout);

If you are using React Navigation, you can imitate this https://github.com/hyochan/react-native-iap/blob/main/IapExample/src/navigators/StackNavigator.tsx#L18

Step 5: The code

Initialize Connection:

import { initConnection, endConnection } from 'react-native-iap';

At the beginning, initialize the connection to the store:
await initConnection();

At the end, if there is a connection, end it by endConnection()

Subscriptions for iOS:

import { getSubscriptions, requestSubscription } from 'react-native-iap';

const subscriptions = await getSubscriptions({ skus: ['your.product.id'] });
const result = await requestSubscription({ sku: 'your.product.id' });

subscriptions:

[{
  "countryCode":"USA",
  "currency":"USD",
  "description":"bla bla",
  "discounts":[
   
  ],
  "introductoryPrice":"",
  "introductoryPriceAsAmountIOS":"",
  "introductoryPriceNumberOfPeriodsIOS":"",
  "introductoryPricePaymentModeIOS":"",
  "introductoryPriceSubscriptionPeriodIOS":"",
  "localizedPrice":"$55.99",
  "platform":"ios",
  "price":"55.99",
  "productId":"your.product.id",
  "subscriptionPeriodNumberIOS":"1",
  "subscriptionPeriodUnitIOS":"MONTH",
  "title":"bla bla",
  "type":"subs"
}]

result:

{
      "productId":"your.product.id",
      "transactionId":"0",
      "transactionReceipt":"MIAGC…xo/4MAAAAAAAA=",
      "transactionDate":1749025582877
}

Subscriptions for Android:

import { getSubscriptions, requestSubscription, useIAP } from 'react-native-iap';

const { subscriptions } = useIAP();  // Note 2: otherwise this subscriptions is empty.

const offerToken = subscriptions[i].subscriptionOfferDetails[j].offerToken;

await getSubscriptions({ skus: ['your.product.id'] }); // Note 1: Need to call this first (eg. in useEffect),
await requestSubscription({
    'your.product.id',
    subscriptionOffers:[
      {
        sku: 'your.product.id',
        offerToken: offerToken,
      }
    ]
});

More info about the required Offer Token in Android: https://github.com/hyochan/react-native-iap/issues/2155#issuecomment-1689152125 .

subscriptions:

[
   {
      "description":"",
      "name":"bla bla",
      "platform":"android",
      "productId":"your.product.id",
      "productType":"subs",
      "subscriptionOfferDetails":[
         {
            "basePlanId":"the-base-plan-name",
            "offerId":null,
            "offerTags":[
               
            ],
            "offerToken":"Xxxx==",
            "pricingPhases":{
               "pricingPhaseList":[
                  "Array"
               ]
            }
         }
      ],
      "title":"bla bla (your.product.id (unreviewed))"
   }
]

result:

[{
  "autoRenewingAndroid":true,
  "dataAndroid": { // STRINGIFIED
      "orderId":"GPA.3304-5343-1082-12882",
      "packageName":"xxx.yyy.zzz",
      "productId":"your.product.id",
      "purchaseTime":1750295050933,
      "purchaseState":0,
      "purchaseToken":"keino...7RZlqY",
      "quantity":1,
      "autoRenewing":true,
      "acknowledged":false
  },
  "developerPayloadAndroid":"",
  "isAcknowledgedAndroid":false,
  "obfuscatedAccountIdAndroid":"",
  "obfuscatedProfileIdAndroid":"",
  "packageNameAndroid":"xxx.yyy.zzz",
  "productId":"your.product.id",
  "productIds":[
      "your.product.id"
  ],
  "purchaseStateAndroid":1,
  "purchaseToken":"keino...7RZlqY",
  "signatureAndroid":"yUzi...vcyww==",
  "transactionDate":1750295050933,
  "transactionId":"GPA.3304-5343-1082-12882",
  "transactionReceipt": { // STRINGIFIED
      "orderId":"GPA.3304-5343-1082-12882",
      "packageName":"xxx.yyy.zzz",
      "productId":"your.product.id",
      "purchaseTime":1750295050933,
      "purchaseState":0,
      "purchaseToken":"keino...7RZlqY",
      "quantity":1,
      "autoRenewing":true,
      "acknowledged":false
  }
}]

Things that may go wrong:

IAP:

Todo: beyond here, I am not sure yet.

import { getProducts } from 'react-native-iap'; 

const products = await getProducts({ skus: ['product_id'] });

Request Purchase:
Trigger the purchase flow:

import { requestPurchase } from 'react-native-iap'; 

await requestPurchase({ sku: 'product_id' });

Handle Purchase Updates:
Listen for purchase updates:

import { purchaseUpdatedListener, finishTransaction } from 'react-native-iap'; 

const purchaseUpdateSubscription = purchaseUpdatedListener(async (purchase) => {
    const { transactionReceipt } = purchase;
    if (transactionReceipt) {
        // Handle transaction, send receipt to your server for validation
        await finishTransaction({ purchase, isConsumable: true });
    }
});

return () => {
    purchaseUpdateSubscription.remove();
};

Note for iOS: https://github.com/hyochan/react-native-iap/blob/main/src/iap.ts#L531

Validate Receipts

Google Play Developer API

If you don't do this step, Google will cancel the purchase after a while.

  1. purchaseToken will be obtained from the mobile app after purchase.
  2. Need OAuth2 access token from Google service account.
  3. Checks if the purchase is valid, refunded, etc:

Create an account on Google Cloud Console first

You also need to add your service account into your Google Play:

Then for it, click "Manage->", under the Account Permissions tab (NOT App Permissions tab), give it all the Financial permissions. Then wait a day or 2 for the changes to propagate through Google's system.

Then generate an JWT:

const jwt = require('jsonwebtoken');

let payload = {
  iss: GOOGLE_CLOUD_SERVICE_ACCOUNT_CLIENT_EMAIL,
  scope: 'https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/pubsub https://www.googleapis.com/auth/androidpublisher',
  aud: 'https://oauth2.googleapis.com/token'
};
let options = {
  expiresIn: '1h',
  algorithm: 'RS256',
  header: {
    alg: 'RS256',
    typ: "JWT"
  }
};
let assertionToken = jwt.sign(payload, GOOGLE_CLOUD_SERVICE_ACCOUNT_PRIVATE_KEY, options);

Then use the JWT to obtain an access token:

curl --location 'https://oauth2.googleapis.com/token' \
--header 'Host: oauth2.googleapis.com' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
--data-urlencode 'assertion={THAT_ASSERTION_TOKEN}'

Then do the receipt verification call:

curl --location --request POST 'https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{purchaseToken}:acknowledge' \
--header 'Authorization: Bearer {THAT_ACCESS_TOKEN}'

will get you "204 No Content". Then generate another assertion token and obtain another access token first. Then:

curl --location 'https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{purchaseToken}' \
--header 'Authorization: Bearer {THAT_ACCESS_TOKEN}'

will get you either this in a success scenario

{
    "kind": "androidpublisher#subscriptionPurchaseV2",
    "startTime": "2025-06-29T03:18:44.779Z",
    "regionCode": "US",
    "subscriptionState": "SUBSCRIPTION_STATE_ACTIVE",
    "latestOrderId": "GPA.111-222-333-4444",
    "testPurchase": {},
    "acknowledgementState": "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
    "lineItems": [
        {
            "productId": "your.product.id",
            "expiryTime": "2025-06-29T03:23:44.201Z",
            "autoRenewingPlan": {
                "autoRenewEnabled": true,
                "recurringPrice": {
                    "currencyCode": "USD",
                    "units": "42",
                    "nanos": 420000000
                }
            },
            "offerDetails": {
                "basePlanId": "your-base-plan-id"
            },
            "latestSuccessfulOrderId": "GPA.111-222-333-4444"
        }
    ]
}

or this in a failure scenario

{
    "kind": "androidpublisher#subscriptionPurchaseV2",
    "startTime": "2025-06-26T07:56:34.121Z",
    "regionCode": “US”,
    "subscriptionState": "SUBSCRIPTION_STATE_EXPIRED",
    "latestOrderId": "GPA.1111-2222-3333-44444”,
    "canceledStateContext": {
        "systemInitiatedCancellation": {}
    },
    "testPurchase": {},
    "acknowledgementState": "ACKNOWLEDGEMENT_STATE_PENDING",
    "lineItems": [
        {
            "productId": “your.product.id”,
            "expiryTime": "2025-06-26T08:01:34.992Z",
            "autoRenewingPlan": {
                "recurringPrice": {
                    "currencyCode": “USD”,
                    "units": "42”,
                    "nanos": 420000000
                }
            },
            "offerDetails": {
                "basePlanId": “your-base-plan-id”
            },
            "latestSuccessfulOrderId": "GPA.1111-2222-3333-44444"
        }
    ]
}

Note: If you ever see an error response like:

Then just follow that link, enable the API

Then wait a few minutes, then try again.

Other resources:

App Store Server API

Begin by following:

  1. Use Private key (.p8) , Key ID & Issuer ID to obtain JWT:
let payload = {
  iss: APP_STORE_CONNECT_API_ISSUER_ID,
  aud: "appstoreconnect-v1",
  bid: "your.bundle.id"
};
const options = {
  expiresIn: '1h',
  algorithm: 'ES256',
  header: {
      alg: "ES256",
      kid: APP_STORE_CONNECT_API_KEY_ID,
      typ: "JWT"
  }
};
let jwtToken = jwt.sign(payload, APP_STORE_CONNECT_API_PRIVATE_KEY, options);
  1. Getting the full purchase data:
let verifyReceiptCallConfigs = {
    method: 'get',
    url: `https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/${transactionId}`,
    // url: `https://api.storekit.itunes.apple.com/inApps/v1/transactions/${transactionId}`,
    headers: { 
        'Authorization': `Bearer ${jwtToken}`
    }
};
let verifyReceiptCallResponse = await axios.request(verifyReceiptCallConfigs);

Important note: If you use StoreKit testing, you will get a "0" for transactionId. So you should use Sandbox testing, in order to get a proper transactionId.

And then you can get a response like:

{
    "signedTransactionInfo": "eyJhb…”
}

Good tutorial about how to make JWT: https://www.youtube.com/watch?v=2ywnYVrzWC8

Other resources:

Data to keep to confirm/refute disputes later

Android

  • purchaseToken
  • orderId
  • productId
  • purchaseTime

There's also:

  • Monetize > Reports > sales & retention data

iOS

  • transactionId
  • productId
  • purchaseDate

There's also:

  • Sales & Trends > aggregate reports
  • Financial Reports > revenue summaries

Fees

Another good tutorial of plain IAP and Subscriptions in React Native

With RevenueCat

FAQ

Testing without real money (ie: with dummy tester)

Android

This tutorial should cover it all: https://www.youtube.com/watch?v=u5p1P0gCmJo
In short, create some Licensed Testers in Google Play Console, verify those emails, then use those accounts in Google Play Store.

Then sign into Play Store using that account. It works on both emulators and real phones.

More documentation: https://developer.android.com/google/play/billing/test

iOS

With real iPhones

Create sandbox tester account here: https://appstoreconnect.apple.com/access/users/sandbox

Follow this tutorial: https://www.youtube.com/watch?v=GeB2ws0YgsQ

Then you also get email to verify your email and may subsequently setup a 2FA.

Enable iPhone's developer mode. Check iPhone > Settings > Privacy and Security. If you don't see Developer Mode, then enable it.

iPhone > Settings > Developer > Sandbox Apple Account

Now get your app via TestFlight. Learn how to use TestFlight here: https://github.com/atabegruslan/Notes/wiki/Testing#testflight

Once you got your app installed, find your subscription button and press it. You will see:

Then click the Apple icon on your Mac's top left > System Settings. Add your Apple account here in order to receive 2FAs.

Then you'll either see successful or unsuccessful result.

More documentation:

With Simulator

StoreKit Testing:

In xCode: Create a StoreKit Config File (named Products) and an InApp Consumable.

Then edit the Schema to include the StoreKit Config file.

Then run Simulator from xCode (Press the Play button near the top left of xCode).

If you are using React Native, then DONT run it via the React Native script in terminal. You must run it from xCode, otherwise the StoreKit config file won't be taken into account.

If you are using React Native Expo, and after you pressed the Play button in xCode, you got the error saying that it cant connect to the server. Then you need to first run in the terminal npx expo run:ios and keep it running. Then run from xCode by pressing the Play button.

Testing long period subscriptions

Android

iOS

More documentations:

Where money come from

Android

GooglePay / Google Play Store, or physically via a shop

iOS

Apple pay (which is just a wrapper around credit cards, paypal, momo, ...) or iTunes wallet

How do money ultimately reach you

Android

Ensure your info is setup here:

More info:

iOS

Ensure your info is setup here:
https://appstoreconnect.apple.com > "Agreements, Tax & Banking"

More info: https://developer.apple.com/help/app-store-connect/getting-paid/overview-of-receiving-payments

Cancellation

https://github.com/hyochan/react-native-iap/blob/main/docs/docs/faq.mdx#how-can-a-user-cancel-a-subscription-in-my-app

Android

How to cancel: https://support.google.com/googleplay/answer/7018481?hl=en&co=GENIE.Platform%3DAndroid

Webhook that fires when cancellation happens: https://developer.android.com/google/play/billing/rtdn-reference

iOS

How to cancel: https://www.youtube.com/shorts/VhEVpWFWG48

Webhook that fires when cancellation happens: https://developer.apple.com/documentation/appstoreservernotifications

More info:


Other general and useful resources

Android

iOS

⚠️ **GitHub.com Fallback** ⚠️