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

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/
