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

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/

Ingeniero en Informática, Investigador, me encanta crear cosas o arreglarlas y darles una nueva vida. Escritor y poeta. Más de 20 APPs publicadas y un libro en Amazon.