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:
- How to set up IAP for iOS and Android.
- Adding products for subscriptions.
- Fetching subscriptions in a React Native application.
- Verifying subscription tokens in the backend.
- Setting up webhooks for iOS and Android.
- Understanding the different types of server notifications.
- 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:
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.
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);
}
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);
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.',
);
}
};
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
Next, click on the topic and create a subscription. After creating the subscription, edit it and change the type from Push to Pull.
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.
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:
- Go back to Google Cloud Platform (GCP) and navigate to the Pub/Sub section.
- Find the topic you created earlier and click on it.
- At the top-right corner of the topic details page, you will see three dots. Click on them to open a dropdown menu.
- Select View Permissions from the dropdown.
Now, create a new permission for the Pub/Sub Publisher role:
- 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
}
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"));
};
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
}
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,
});
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.
- You need to complete the Agreements, Tax and Bankings.
Add In-App Purchases in Features tab. Check that your product’s status is Ready to Submit.
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)
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)