Implementar compras en aplicación usando expo-iap en React Native Expo para Android /iOS

Tiempo de lectura: 4 minutos

Hoy vamos a aprender a implementar compras in app usando Expo IAP para Android o iOS en React Native.

Furgoneta power - Pexels

Primero vamos a instalar la librería que necesitamos (expo-iap):

npx expo install expo-iap

Ahora tenemos que añadir dentro de app.config.js nuestro código:

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

Recuerda que debes tener creados unas suscripciones dentro de Google Play o Apple Store.

Una vez instalada, vamos a realizar el código:

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;

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

            if (isValidated) {


                // 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 compra: ComprasIAP = {
            id_user: userData?.user_id!,
            purchaseToken: transactionReceipt.purchaseToken,
            packageName: transactionReceipt.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',
    }
});


Y creamos el objeto, dentro de ComprasIAP_Obj

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

Y ahora ya tenemos listo nuestro componente para solicitar compras.

Después para validar la compra, la envio al servidor.

Tienes un ejemplo de cómo validar la compra usando Python. Recomiendo validar las compras, aunque no es obligatorio.

Para validar las compras en back: https://devcodelight.com/validar-compras-en-aplicacion-android-usando-python/

O un ejemplo de cómo validar la compra en iOS usando PHP: https://devcodelight.com/validar-compra-en-aplicacion-compras-in-app-de-ios-usando-php/

Deja un comentario