Añadir suscripciones in app usando React Native en Android o iOS con react-native-iap

Tiempo de lectura: 3 minutos

Hoy vamos a aprender cómo podemos añadir suscripciones en nuestra aplicación desarrollada con React Native para Android o iOS.

Casa con hielo - pexels

Para realizar las compras en aplicación tenemos que instalar la siguiente librería en nuestro proyecto:

npm install react-native-iap

Os recomiendo instalar la versión (a fecha de hoy):

"react-native-iap": "12.16.3",

Referencias de compras de suscripción en Android:

https://developer.android.com/google/play/billing/rtdn-reference?hl=es-419

Referencias de compras de suscripción en iOS:

https://developer.apple.com/documentation/appstoreservernotifications

Y ahora implementamos el siguiente código qué nos permitirá realizar la compra:

import React, { useEffect, useState } from 'react';
import {
  EmitterSubscription,
  Linking,
  Platform,
  SafeAreaView,
  ScrollView,
  StyleSheet,
  Text,
  View,
  Button,
  TouchableOpacity,
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useAuthContext } from '@/context/AuthContext';
import { AxiosResponse } from 'axios';
import * as RNIap from 'react-native-iap';
import { ComprasObj } from '@/objects/ComprasIAP';
import { useServices } from '@/context/ServiceContext';

const test = false;

const productosSolicitados = Platform.select({
  ios: ['suscripcion.mes.1'],
  android: ['suscripcion.mes.1'],
});

const ComprarSuscripcionScreen: React.FC = () => {
  const { t } = useTranslation();
  const { userData } = useAuthContext();
  const { comprasInAppQueries } = useServices();

  const [productos, setProductos] = useState<RNIap.Subscription[]>([]);

  useEffect(() => {
    let retornoCompraError: EmitterSubscription | null = null;
    let retornoCompraUpdate: EmitterSubscription | null = null;

    RNIap.initConnection()
      .then(async () => {
        console.log('Conectado IAP');

        try {
          const products = await RNIap.getSubscriptions({
            skus: productosSolicitados!,
          });

          products.sort((a, b) => {
            const aPrice = (a as any)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[0]?.priceAmountMicros ?? 0;
            const bPrice = (b as any)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[0]?.priceAmountMicros ?? 0;
            return aPrice - bPrice;
          });

          setProductos(products);
        } catch (error) {
          console.log('Error buscando suscripciones: ' + error);
        }

        retornoCompraError = RNIap.purchaseErrorListener((error) => {
          console.log('Error compra: ' + JSON.stringify(error));
        });

        retornoCompraUpdate = RNIap.purchaseUpdatedListener(async (purchase) => {
          try {
            if (Platform.OS === 'android' && !purchase.isAcknowledgedAndroid && purchase.purchaseToken) {
              await RNIap.acknowledgePurchaseAndroid({ token: purchase.purchaseToken });
            }

            await RNIap.finishTransaction({ purchase, isConsumable: false });

            const objCompra: ComprasObj = {
              user_uuid: userData!.user_uuid,
              purchaseToken: purchase.purchaseToken ?? purchase.transactionReceipt,
              packageName: purchase.packageNameAndroid ?? '',
              productId: purchase.productId,
              transactionId: purchase.transactionId ?? '',
              purchaseTime: purchase.transactionDate,
              platform: Platform.OS,
              test: test,
            };

            comprasInAppQueries?.sendSubscriptionFinished(objCompra)
              .then((response: AxiosResponse<string>) => {
                console.log('Compra enviada al backend: ', response.data);
              })
              .catch((error) => {
                console.log('Error enviando compra al backend: ', error);
              });
          } catch (err) {
            console.warn('Error procesando compra:', err);
          }
        });
      })
      .catch((error) => {
        console.log('Error conectando IAP: ' + error);
      });

    return () => {
      retornoCompraUpdate?.remove();
      retornoCompraError?.remove();
      RNIap.endConnection().catch((e) => console.log('Error cerrando conexión:', e));
    };
  }, []);

  const comprar = (producto: RNIap.Subscription) => {
    const offerToken = (producto as any)?.subscriptionOfferDetails?.[0]?.offerToken;

    RNIap.requestSubscription({
      sku: producto.productId,
      ...(offerToken && {
        subscriptionOffers: [{ sku: producto.productId, offerToken }],
      }),
    }).catch((error) => {
      console.log('❌ Error en la compra: ', error);
    });
  };

  const cancelarSuscripcion = (productId: string) => {
    const url = Platform.select({
      android: `https://play.google.com/store/account/subscriptions?sku=${productId}&package=com.tuapp.package`,
      ios: 'https://apps.apple.com/account/subscriptions',
    });
    Linking.openURL(url!);
  };

  return (
    <SafeAreaView style={styles.contenedorGeneral}>
      <ScrollView>
        <View style={styles.contenedorPremium}>
         
          <View style={styles.contenedorBotones}>
            {productos.map((pkg) => {
              const price = (pkg as any)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[0]?.formattedPrice ?? '';
              return (
                <TouchableOpacity
                  key={pkg.productId}
                  style={styles.boton}
                  onPress={() => comprar(pkg)}
                >
                  <Text style={styles.botonTexto}>{`${t(pkg.productId)} ${price}`}</Text>
                </TouchableOpacity>
              );
            })}
          </View>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

export default ComprarSuscripcionScreen;

const styles = StyleSheet.create({
  contenedorGeneral: {
    flex: 1,
    width: '100%',
    height: '100%',
    backgroundColor: '#fff',
  },
  contenedorPremium: {
    padding: 20,
    alignItems: 'center',
  },
  card: {
    backgroundColor: '#f2f2f2',
    borderRadius: 10,
    padding: 20,
    width: '100%',
    marginBottom: 20,
  },
  premiumTitulo: {
    fontSize: 20,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 10,
  },
  premiumSubtitulo: {
    fontSize: 16,
    fontWeight: '400',
    marginBottom: 10,
    textAlign: 'justify',
  },
  premiumTexto: {
    fontSize: 15,
    marginBottom: 5,
  },
  contenedorBotones: {
    width: '100%',
    alignItems: 'center',
  },
  boton: {
    backgroundColor: '#007bff',
    paddingVertical: 12,
    paddingHorizontal: 20,
    borderRadius: 8,
    marginVertical: 6,
    width: '100%',
    alignItems: 'center',
  },
  botonTexto: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

NOTA: uso las i18n para devolver el valor en texto de los elementos de compra.

Con esto tendremos las compras implementadas.

Cómo se informa al backend de actualizaciones de suscripciones (renovaciones, cancelaciones, etc.)

1. Cuando el usuario compra o renueva dentro de la app

  • El cliente (tu app React Native) recibe la notificación en tiempo real vía purchaseUpdatedListener.
  • Entonces tú mandas el recibo o token al backend para validar y actualizar el estado del usuario.

Esto ya lo tienes implementado con purchaseUpdatedListener.

2. Pero las suscripciones pueden renovarse, cancelarse o expirar fuera de la app

Por ejemplo:

  • Renovación automática mensual
  • Cancelación desde Google Play o App Store (fuera de la app)
  • Cambios de plan, reembolsos, etc.

Tu backend no sabe automáticamente cuándo esto ocurre si solo confía en el cliente.

Deja un comentario