DEV Community

Ali Osaid
Ali Osaid

Posted on

In App Purchase ( Node + React-Native

A Complete Guide to Setting Up In-App Purchases with Google Cloud Platform and Apple App Store

In this blog post, we will cover the following topics to help you implement In-App Purchases (IAP) seamlessly:

  1. How to set up IAP for iOS and Android.
  2. Adding products for subscriptions.
  3. Fetching subscriptions in a React Native application.
  4. Verifying subscription tokens in the backend.
  5. Setting up webhooks for iOS and Android.
  6. Understanding the different types of server notifications.
  7. Handling various notification types effectively.

How to set up IAP for iOS and Android

Open the Google Cloud Platform (GCP) and navigate to API & Services. You will see an interface like this:

Image description

Click on the Enable API & Services button. This will take you to a new screen with a search bar. Type "Google Cloud APIs" in the search bar and enable it.

Next, navigate to the Google Play Console.

However, you cannot add products or subscriptions until you enable billing. To do this, push a build to the Google Play Console first.

Add the following dependency to your build.gradle file:
implementation 'com.android.billingclient:billing:5.0.0'

Also, add the following permission to your AndroidManifest.xml file:
<uses-permission android:name="com.android.vending.BILLING" />

n the Google Play Console, select your app. Then, on the left sidebar, you will see the Monetize and Play tab. Click on it to open a dropdown menu.

From the dropdown, select the Products tab, which will further expand. In this expanded menu, you will see the Subscription option.

Image description

Here, you can assign a unique product ID and name, such as Gold or Silver Subscription.

Next, you can add a Base Plan. For example, a Gold Subscription can be set up for daily, monthly, or yearly renewals. The Base Plan allows you to define details like auto-renewal, prepaid options, and pricing. It’s a simple and straightforward process to configure.

After setting this up, you’ll need to install the required library:
npm install react-native-iap

import * as RNIap from 'react-native-iap';
...
try {
  await RNIap.prepare();
  const products = await RNIap.getProducts(itemSkus);
   setProducts(products) 
} catch(error) {
  console.log("ERROR ->",error);
}
Enter fullscreen mode Exit fullscreen mode
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import {
  initConnection,
  endConnection,
  getSubscriptions,
  purchaseUpdatedListener,
  purchaseErrorListener,
  Subscription,
} from 'react-native-iap';
import { Platform } from 'react-native';
import { EmitterSubscription } from 'react-native';

// Define the context type for better typing
interface IAPContextType {
  subscriptions: Subscription[];
  isConnected: boolean;
  fetchSubscriptions: () => void; // Add fetchSubscriptions function to the context
}

const IAPContext = createContext<IAPContextType>({
  subscriptions: [],
  isConnected: false,
  fetchSubscriptions: () => {},
});

// Define props type for the provider
interface IAPProviderProps {
  children: ReactNode;
}

export const IAPProvider = ({ children }: IAPProviderProps) => {
  const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const [purchaseListener, setPurchaseListener] = useState<EmitterSubscription | null>(null);
  const [errorListener, setErrorListener] = useState<EmitterSubscription | null>(null);

  // Define SKU lists
  const androidSKUs = ['GOLD', 'SIKVER']; // Replace with your Android product IDs
  const iosSKUs = ['SILVER', 'GOLD']; // Replace with your iOS product IDs

  // Initialize IAP connection and fetch subscriptions
  const initializeIAP = async () => {
    try {
      const connected = await initConnection();
      setIsConnected(connected);
      if (connected) {
        console.log('IAP Connected');
        fetchSubscriptions();
      } else {
        console.error('IAP Connection Failed');
      }
    } catch (error) {
      console.error('IAP Initialization Error:', error);
    }
  };

  // Fetch subscriptions based on platform (Android/iOS)
  const fetchSubscriptions = async () => {
    try {
      const skus = Platform.OS === 'android' ? androidSKUs : iosSKUs;
      const products = await getSubscriptions({ skus });
      setSubscriptions(products);
      console.log('Fetched Subscriptions:', products);
    } catch (error) {
      console.error('Error fetching subscriptions:', error);
    }
  };

  // Set up purchase and error listeners
  useEffect(() => {
    initializeIAP();

    const purchaseSub = purchaseUpdatedListener((purchase) => {
      console.log('Purchase Updated:', purchase);
      // Handle purchase success
    });

    const errorSub = purchaseErrorListener((error) => {
      console.error('Purchase Error:', error);
      // Handle purchase error
    });

    setPurchaseListener(purchaseSub);
    setErrorListener(errorSub);

    // Cleanup listeners and close connection when the component unmounts
    return () => {
      console.log('Cleaning up IAP connection...');
      endConnection();
      purchaseListener?.remove();
      errorListener?.remove();
    };
  }, [purchaseListener, errorListener]);

  return (
    <IAPContext.Provider value={{ subscriptions, isConnected, fetchSubscriptions }}>
      {children}
    </IAPContext.Provider>
  );
};

// Custom hook for accessing IAP context
export const useIAP = () => useContext(IAPContext);

Enter fullscreen mode Exit fullscreen mode

Now create susbcription Screen

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

const handleSubscriptionSelect = async (
    productId: string,
    offerToken: string,
  ) => {
    try {
      // eslint-disable-next-line prettier/prettier
      console.log(`Subscribing to Product: ${productId}, with OfferToken: ${offerToken}`);
      console.log('Attempting subscription:', {
        sku: productId,
        subscriptionOffers: [{offerToken}],
      });
      const purchase = await requestSubscription({
        sku: productId,
        subscriptionOffers: [{sku: productId, offerToken}],
      });
      console.log('Purchase successful:', purchase);
      // Finish the transaction to acknowledge the purchase
      if (purchase) {
        if (Array.isArray(purchase)) {
          for (const p of purchase) {
            await finishTransaction({purchase: p});
          }
        } else {
          await finishTransaction({purchase});
        }
      } else {
        throw new Error('Purchase was not successful.');
      }
      Alert.alert('Success', 'You have successfully subscribed!');
    } catch (error: any) {
      console.error('Purchase error:', error);
      Alert.alert(
        'Purchase Failed',
        error.message || 'An unknown error occurred.',
      );
    }
  };
Enter fullscreen mode Exit fullscreen mode

That’s it for the code part! You should now be able to see the subscription you added in the Google Play Console.

If the products aren’t fetched, it’s likely due to a configuration issue. On iOS or Android, the most common causes are:

Incorrect configuration.
Testing on an emulator instead of a physical device (this issue is mostly occur on iOS).

Let’s configure the webhook to listen for IAP events on the backend.

First, navigate back to the GCP console. Go to Pub/Sub, where you’ll need to create a topic.

After creating a topic

Image description

Next, click on the topic and create a subscription. After creating the subscription, edit it and change the type from Push to Pull.

Image description

Then, you need to specify the URL where you will receive the webhook events.

Make sure to check the Retry Policy at the bottom of the page. Set it to Backup with a specific interval. This will prevent the webhook event from being re-sent repeatedly in case of failure. For development purposes, we don't want it to resend the event continually, so we'll enable this policy in production.

After this, go to the Google Play Console. In the Monetize and Play tab, you will see the Monetization Setup section. Click on it, and copy the topic name you provided in GCP. Paste it into the input field.

Image description

Now, push your code and test the server notifications. You may encounter an error that your topic is incorrect. This usually happens if you've missed a step in the setup process. To resolve this:

  1. Go back to Google Cloud Platform (GCP) and navigate to the Pub/Sub section.
  2. Find the topic you created earlier and click on it.
  3. At the top-right corner of the topic details page, you will see three dots. Click on them to open a dropdown menu.
  4. Select View Permissions from the dropdown.

Now, create a new permission for the Pub/Sub Publisher role:

  1. Add the following service account to the permissions list google-play-developer-notifications@system.gserviceaccount.com

Test Notifications in Google Play Console
Once you've updated the permissions, go back to the Google Play Console and test the server notification again. If the notification keeps coming in without stopping, it means the system is still waiting for an acknowledgment.

Acknowledge the Webhook
To confirm receipt of the webhook event and stop the repeated notifications, you need to respond with a 200 OK status code. This ensures that the event was successfully received and processed.

interface SubscriptionNotification {
  version: string; 
  notificationType: number; 
  purchaseToken: string;  
  subscriptionId: string; 
}

export interface AndroidSubscriptionWebhookEvent {
  version: string; 
  packageName: string; 
  eventTimeMillis: string; 
  subscriptionNotification:SubscriptionNotification
}
Enter fullscreen mode Exit fullscreen mode

The data we get from android and IOS webhook server notification is encrpy so we need to decode it

create a method to decode the data


  decodeJWT = (jwt: string): AndroidSubscriptionWebhookEvent => {
    return JSON.parse(Buffer.from(jwt, "base64").toString("utf-8"));
  };

Enter fullscreen mode Exit fullscreen mode

this is the different type of notification that you most likly will interact with

export enum WEBHOOK_ANDROID {
  "renewed" = 2,
  "cancel" = 3,
  "purchased" = 4,
  "revoked" = 12,
  "expired" = 13
}
Enter fullscreen mode Exit fullscreen mode

now you can implement your logic further according to your DB structure

but also you have to verify the Receipt that purchase in the FE for that you have to make an end point in Backend

const { applicationId, subscriptionId, purchaseToken, platform = PLATFORM_TYPE.ANDROID} = req.body;

 const auth = new google.auth.GoogleAuth({
        keyFile: "./googleapis.json",
        scopes: ["https://www.googleapis.com/auth/androidpublisher"],
    });

    const androidPublisher = google.androidpublisher({
        auth,
        version: "v3",
        params:{sandbox:true}  // make it false in production
    });

    try {
        const response = await androidPublisher.purchases.subscriptions.get({
            packageName,
            subscriptionId,
            token,
        });
Enter fullscreen mode Exit fullscreen mode

this will verify the subscription and then you can save the data into DB

Let's cover the IOS side

Setup your in app products for ios in itunesconnect.

Image description

  1. You need to complete the Agreements, Tax and Bankings.
  2. Add In-App Purchases in Features tab. Check that your product’s status is Ready to Submit.

  3. Check your xcode setting and make In-App Purchase available.

Now, you’re done with ios side configuration

use the same package and fetch the subscription products but the payload is different from android and ios so handle it accordingly

FOR IOS Webhook

 const base64EncodedJWT = req.body.signedPayload;
        if (!base64EncodedJWT) return res.status(200).json({ message: "signedPayload is missing from the request body" });

        const payload = decodeJWT(base64EncodedJWT);
        let renewalInfo;
        let transactionInfo;

        if (payload?.data?.signedRenewalInfo) renewalInfo = decodeJWT(payload.data.signedRenewalInfo);
        if (payload?.data?.signedTransactionInfo) transactionInfo = decodeJWT(payload.data.signedTransactionInfo);

        console.log("renewalInfo ->",renewalInfo)
        console.log("transactionInfo ->",transactionInfo)
Enter fullscreen mode Exit fullscreen mode

you need to decode the payload as well you can use the same method that we write earlier to decode the object of server notification of android

DID_RENEW
SUBSCRIBED
CANCEL
EXPIRED

Handle these subscription according to your DB structure

if you like the post do follow me on Github
https://github.com/ali-osaid01

Top comments (0)