Implementing In-App Purchases using expo-iap in React Native Expo for Android/IOS

Tiempo de lectura: 4 minutos

Today we’re going to learn how to implement in-app purchases using Expo IAP for Android or iOS on React Native.

Furgoneta power - Pexels

First we’re going to install the library we need (expo-iap):

npx expo install expo-iap

Now we need to add our code inside app.config.js:

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

Remember to create subscriptions inside Google Play or Apple Store.

Once installed, we will proceed with the code:

import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { View, StyleSheet,Button, ScrollView, Platform, Alert, InteractionManager, FlatList } from "react-native";

import { useAuthContext } from "@/context/AuthContext";
import { useServices } from "@/context/ServicesProvider";
import { AxiosResponse } from "axios";
import { NavigationProp, useNavigation } from "@react-navigation/native";
import { RootParamList } from "@/navigator/AppNavigation"; 
import Rewarded, { initLoadRewardedAds } from "@/components/widgets/anuncios/Rewarded";
import TextoContenedor from "@/components/elements/TextoContenedor";
import { playCash } from "@/util/SoundManager";
import { vibracionCompra } from "@/util/Vibracion";
import {
    ProductPurchase, Purchase, requestProducts, useIAP, validateReceipt,
    getPurchaseHistories
} from "expo-iap";
import { ComprasIAP } from "@/objects/ComprasIAP_Obj";


const test = false;

const productosSolicitados: string[] = Platform.select({
    ios: [
        'producto.1',
        'producto.2'
    ],
    android: [
         'producto.1',
        'producto.2'
    ],
    default: [
            'producto.1',
        'producto.2'
    ],
}) ?? [];

const ComprasIAPScreen: React.FC = ({ }) => {

    const { t } = useTranslation();
    const { userData, creditos, setCreditos } = useAuthContext();

    const navigation = useNavigation<NavigationProp<RootParamList>>();

    const { comprasQueries, activateLoading, activateLoadingAux } = useServices();

    const [monedasPorVideo, setMonedasPorVideo] = useState<number>(0);

    const [videosRestantes, setVideosRestantes] = useState<number>(0);
    const [disableVideos, setDisableVideos] = useState<boolean>(false);
    const [activarRewardedVideo, setActivarRewardedVideo] = useState<boolean>(false);

    const { connected, products, currentPurchase, currentPurchaseError, requestProducts,
        requestPurchase, finishTransaction, validateReceipt } = useIAP();


    useEffect(() => {
        if (!connected) return;

        const clearPendingPurchases = async () => {
            try {
                console.log('🧹 Limpiando compras pendientes...');

                // Obtener historial de compras según la plataforma
                const purchaseHistory = await getPurchaseHistories();

                if (purchaseHistory && Array.isArray(purchaseHistory)) {
                    console.log(`📋 Encontradas ${purchaseHistory.length} compras en historial`);

                    // Filtrar solo nuestros productos y compras no finalizadas
                    const pendingPurchases = purchaseHistory.filter(purchase =>
                        productosSolicitados.includes(purchase.productId)
                    );

                    console.log(`🔄 Procesando ${pendingPurchases.length} compras pendientes`);

                    // Finalizar cada compra pendiente
                    for (const purchase of pendingPurchases) {
                        try {
                            await finishTransaction({
                                purchase,
                                isConsumable: true,
                            });
                            console.log(`Compra finalizada: ${purchase.productId}`);
                        } catch (err) {
                            console.warn(`Error finalizando compra ${purchase.productId}:`, err);
                        }
                    }
                } else {
                    console.log('No hay historial de compras disponible');
                }

            } catch (err) {
                console.error('Error limpiando compras pendientes:', err);
            }
        };

        clearPendingPurchases();
    }, [connected]);

    useEffect(() => {
        if (!connected) return;

        const initializeIAP = async () => {
            try {
                // Get both products and subscriptions
                await requestProducts({ skus: productosSolicitados, type: 'inapp' });
                //await requestProducts({skus: subscriptionSkus, type: 'subs'});

            } catch (error) {
                console.error('Error initializing IAP:', error);
            }
        };

        initializeIAP();
    }, [connected, requestProducts]);

    // Handle successful purchases
    useEffect(() => {
        if (currentPurchase) {
            handlePurchaseUpdate(currentPurchase);
        }
    }, [currentPurchase]);

    // Handle purchase errors
    useEffect(() => {
        if (currentPurchaseError) {
            activateLoadingAux.current = false;

            // Don't show error for user cancellation
            if (currentPurchaseError.code === 'E_USER_CANCELLED') {
                return;
            }

            Alert.alert(
                'Purchase Error',
                'Failed to complete purchase. Please try again.',
            );
            console.error('Purchase error:', currentPurchaseError);
        }
    }, [currentPurchaseError]);


    const handlePurchaseUpdate = async (purchase: any) => {
        try {
            activateLoadingAux.current = true;
            console.log('Processing purchase:', purchase);

            //const transactionId = purchase.id;

            const transactionReceipt = Platform.OS === 'ios' ? JSON.parse(purchase.transactionReceipt) : JSON.parse(purchase.transactionReceipt);
            const productId = transactionReceipt.productId;

       
            const isValidated = await recordPurchaseInDatabase(purchase, transactionReceipt);
     
            if (isValidated) {

            // Finish the transaction
            await finishTransaction({
                purchase,
                isConsumable: true, // Set to true for consumable products
            });


                // Update local state (e.g., add bulbs, enable premium features)
                await updateLocalState(productId);

                // Show success message
                showSuccessMessage(productId);
            } else {
                Alert.alert(
                    'Validation Error',
                    'Purchase could not be validated on server. Please contact support.',
                );
            }
        } catch (error) {
            console.error('Error handling purchase:', error);
            Alert.alert('Error', 'Failed to process purchase.');
        } finally {
            activateLoadingAux.current = false;
        }
    };

    //Enviar al servidor:
    const recordPurchaseInDatabase = async (purchase: any, transactionReceipt: any): Promise<boolean> => {

       const transactionReceiptObj = JSON.parse(purchase.transactionReceipt);
        let purchaseToken = "";
        const productId = transactionReceiptObj.productId;

        if (Platform.OS === 'ios') {

            purchaseToken = purchase.purchaseToken;
        } else {
            purchaseToken = transactionReceiptObj.purchaseToken;
        }

        const packageName = Platform.OS === 'ios'
            ? "com.your_bundle_id"// obtiene el bundleId
            : transactionReceipt.packageName;


        const compra: ComprasIAP = {
            id_user: userData?.user_id!,
            purchaseToken: purchaseToken,
            packageName: packageName,
            productId: transactionReceipt.productId,
            transactionId: purchase.transactionId ?? '',
            purchaseTime: purchase.purchaseTime || Date.now(),
            platform: Platform.OS,
            test: test
        };

        try {
            const response: AxiosResponse<number> = await comprasQueries.comprarCreditosIAP(compra);
            if (response.data == 1) {
                console.log("Purchase validated and recorded successfully");
                activateLoadingAux.current = false;
                return true;
            } else {
                console.error('Server validation failed:', response.data);
                return false;
            }
        } catch (error) {
            console.error('Error recording purchase:', error);
            return false;
        }
    };

    //Actualizar en local
    const updateLocalState = async (productId: string) => {
        // Update your local app state based on the purchase
        if (productosSolicitados.includes(productId)) {
            //Obtener el valor de las monedas compradas
            const monedas = productId.split('.')[1];
            console.log(`Adding ${monedas} monedas to user account`);
            setCreditos(creditos + parseInt(monedas));
        }
    };

    const showSuccessMessage = (productId: string) => {
        InteractionManager.runAfterInteractions(() => {
            if (productosSolicitados.includes(productId)) {
                const monedas = productId.split('.')[1];
                Alert.alert(
                    'Thank You!',
                    `${monedas} monedas have been added to your account.`,
                );
            }
        });
    };

    // Request purchase for products
    const solicitarCompraProducto = async (productId: string) => {
        if (!connected) {
            Alert.alert(
                'Not Connected',
                'Store connection unavailable. Please try again later.',
            );
            return;
        }

        try {
            activateLoadingAux.current = true;

            // Platform-specific purchase request (v2.7.0+)
            await requestPurchase({
                request: {
                    ios: {
                        sku: productId,
                        andDangerouslyFinishTransactionAutomatically: false,
                    },
                    android: {
                        skus: [productId],
                    },
                },
            });
        } catch (error) {
            activateLoadingAux.current = false;
            console.error('Purchase request failed:', error);
        }
    };

    return (
        <View style={styles.contenedor}>
            {/*Lista de productos*/}
            <FlatList
                data={products}
                renderItem={({ item }) => (
                   <Button
                      onPress={() => {
                            if (!activateLoadingAux.current) {
                                solicitarCompraProducto(item.id);
                            }
                        }}
                      title="Producto " + item.id + " (" + item.displayPrice?.toString() +")"
                      color="#841584"
                      accessibilityLabel="Learn more about this purple button"
                   />
                )}
                keyExtractor={(item) => item.id}
            />
        </View>
    )
};

export default ComprasIAPScreen;


const styles = StyleSheet.create({
    contenedor: {
        flex: 1,
        width: '100%',
        justifyContent: 'center',
    }
});


Change const packageName = Platform.OS === 'ios'
? "com.your_bundle_id"// obtiene el bundleId
transactionReceipt.packageName;

And create new object ComprasIAP_Obj:

export interface ComprasIAP_Obj {
    id_user: number;
    purchaseToken: string;
    packageName: string;
    productId: string;
    transactionId: string;
    purchaseTime: number;
    platform: string;
    test: boolean;
}

You have an example of how to validate a purchase using Python. I recommend validating purchases, although it is not obligatory.

To validate purchases in the back: https://devcodelight.com/validar-compras-en-aplicacion-android-usando-python/

O an example of how to validate a purchase in iOS using PHP: https://devcodelight.com/validar-compra-en-aplicacion-compras-in-app-de-ios-usando-php/

Leave a Comment