Las compras dentro de la aplicación nativas de React: implementación sencilla. Tutorial
Updated: mayo 23, 2023
25 min read
Los frameworks de desarrollo de aplicaciones multiplataforma ciertamente facilitan la vida de los desarrolladores, permitiéndoles crear aplicaciones para varias plataformas a la vez. Sin embargo, existen algunas desventajas. Por ejemplo, React Native no tiene ninguna herramienta preparada para implementar las compras dentro de la aplicación (in-app purchases). Por lo tanto, tendrás que recurrir inevitablemente a bibliotecas de terceros.
Qué opciones hay para implementar las compras dentro de la aplicación
Las bibliotecas más populares para las suscripciones (subscriptions) dentro de la app en las aplicaciones React Native son react-native-iap y expo-in-app-purchases. Sin embargo, hablaré de react-native-adapty, porque tiene bastantes ventajas en comparación con las otras bibliotecas:
- A diferencia del resto, proporciona una validación de compra (purchase validation) basada en el servidor.
- Es compatible con todas las funciones implementadas recientemente por las tiendas de aplicaciones, desde las ofertas promocionales (promo offers) hasta las funciones de pago por adelantado (pay-upfront). Además, es rápida para admitir las nuevas funciones que surjan.
- El código acaba siendo más claro y sencillo.
- Puedes modificar tu oferta del producto y añadir o eliminar nuevas ofertas sin tener que pasar por el ciclo completo de lanzamiento. No hay necesidad de lanzar versiones beta y esperar a la aprobación.
El SDK (SDK) de Adapty es mucho más que eso. Tienes herramientas de análisis integradas para todas las métricas clave, análisis de cohortes (cohort analysis), validación de compra basada en el servidor, pruebas AB (AB testing) para muros de pago, campañas de promoción con segmentación flexible, integraciones de herramientas de análisis de terceros y mucho más.
En este artículo
Por ahora, vamos a hablar de la configuración de las compras dentro de la aplicación en las aplicaciones React Native. Esto es lo que vamos a cubrir hoy:
- Por qué la Expo no funcionará para las compras dentro de la aplicación en las aplicaciones React Native.
- Crear una cuenta de desarrollador.
- Configurar Adapty:
Configurar la App Store
Configurar la Play Store - Añadir suscripciones.
- Crear un muro de pago.
- Instalar react-native-adapty.
- Un ejemplo de aplicación y el resultado.
En esta guía, intentaremos crear una aplicación que muestre imágenes de gatos a los usuarios suscritos y que ofrezca a todos los demás una oferta de suscripción.
Por qué la Expo no funcionará para las compras dentro de la aplicación en las aplicaciones React Native
Resumiendo: la Expo «gestionada» no es compatible con los métodos nativos que ofrecen las tiendas de aplicaciones para procesar las compras (también conocidos como kits de tienda). Tendrás que ceñirte al RN puro o utilizar el flujo de trabajo de Expo bare.
De entrada, tendré que decepcionar a los que pensaron en usar Expo: esto no funcionará. Expo es un framework React Native que facilita mucho el desarrollo de aplicaciones. Sin embargo, su flujo de trabajo gestionado no es compatible con el procesamiento de compras/suscripciones. Expo no utiliza ningún código nativo en sus métodos y componentes (ambos son sólo JS), lo cual es necesario para los kits de tienda. No hay forma de implementar las compras dentro de la aplicación en las tiendas móviles con JavaScript, así que tendrás que «expulsar».
Crear una cuenta de desarrollador
En primer lugar, tendrás que configurar las cuentas de la tienda de aplicaciones, así como crear y configurar las compras y las suscripciones tanto para iOS como para Android. Esto no debería llevarte más de 20 minutos.
Si todavía no has configurado tu cuenta de desarrollador y tus productos en App Store Connect y/o Google Play Console, consulta estas guías:
- Para iOS: lee la guía desde el principio y hasta el epígrafe «Obtener la lista de SKProduct», ya que es donde empezamos a hablar de las implementaciones nativas.
- Para Android: lee la guía desde el principio y hasta el encabezado «Obtener una lista de productos en una aplicación».
Configurar Adapty
Para react-native-adapty, primero tendrás que configurar tu dashboard de Adapty. Esto no te llevará mucho tiempo, pero te proporcionará todas las ventajas mencionadas anteriormente que tiene Adapty sobre el codificado de forma rígida.
En el tercer paso, se te pedirá que configures la App Store y Google Play.
Para iOS, necesitarás:
- Especificar el ID del paquete;
- Configurar las notificaciones del servidor (server notifications) de la App Store;
- Especificar el secreto compartido (shared secret) de App Store Connect.
Estos campos son necesarios para que las compras funcionen.
Cada campo tiene una sugerencia de «Leer cómo» que contiene guías paso a paso. Consúltalas si tienes alguna duda.
El ID del paquete es el ID único de tu aplicación. Debe coincidir con el que hayas especificado en Xcode, in Targets > [App Name] > General:
Para Android, los campos necesarios son el Nombre del Paquete y el Archivo de Clave de la Cuenta de Servicio. Todos estos campos tienen también sus propias sugerencias para Leer cómo. El nombre del paquete hace en Android lo mismo que el ID del paquete en iOS. Debe coincidir con el que hayas especificado en tu código, que se encuentra en el archivo /android/app/build.gradle en android.defaultConfig.applicationId:
En el cuarto paso, se te pedirá que conectes el SDK de Adapty a tu aplicación. Omite este paso por ahora, aunque volveremos a él un poco más tarde.
Una vez que te hayas registrado, comprueba la pestaña de configuración y recuerda que aquí es donde se encuentra tu clave de SDK pública. Necesitarás la clave más adelante.
Añadir una suscripción
Adapty utiliza productos para diferentes suscripciones. Tu suscripción de fotos de gatos puede ser semanal, bianual o anual. Cada una de estas opciones será un producto separado de Adapty.
Vamos a especificar en el dashboard que tenemos un producto. Para ello, ve a Products & A/B Tests → Products y haz clic en Create product.
Aquí tendrás que especificar el nombre del producto, es decir, cómo se verá esta suscripción en tu dashboard de Adapty.
También tendrás que especificar el ID de producto de App Store y el ID de producto de Play Store. Si quieres, especifica también el periodo y el nombre para los análisis. Haz clic en Save.
Crear un muro de pago (paywall)
Ahora, tendrás que diseñar un muro de pago, que es una pantalla que restringe el acceso del usuario a las funciones premium y le hace una oferta de suscripción. Tendrás que añadir el producto que has creado a tu muro de pago. Para ello, haz clic en Create paywall en la misma sección (Products & A/B Tests → Paywalls).
- Elige un nombre de muro de pago tal que tú y tu equipo puedan deducir fácilmente, con sólo mirar el nombre, de qué muro de pago se trata.
- Utilizarás el ID del muro de pago para mostrarlo en tu aplicación. Como ejemplo de nuestra aplicación, usaremos «cats_paywall».
- En el menú desplegable Producto, selecciona tu suscripción.
Haz clic en Save & publish
Eso es todo para la configuración. Ahora, añadiremos las dependencias y escribiremos el código.
Instalar react-native-adapty
1. Primero, añade la dependencia:
yarn add react-native-adapty
2. Instala los pods de iOS. Si aún no tienes el pod CLI, te recomiendo encarecidamente que lo descargues. Sin duda lo vas a necesitar a menudo en el desarrollo de iOS.
#pods get installed into the native iOS project, which, by default, is the /ios folderpod install --project-directory=ios
3. Como los proyectos React Native de iOS están escritos en Obj-C, tendrás que crear un encabezado de puente Swift para que Obj-C pueda leer las bibliotecas Swift. Para ello, sólo tienes que abrir tu proyecto de Xcode y crear un nuevo archivo Swift. Xcode te preguntará si quieres crear un encabezado de puente, que es exactamente lo que quieres. Haz clic en Create (Crear).
4. Para Android, asegúrate de que el proyecto-/android/build.gradle por defecto- utiliza el plugin kotlin-gradle de la versión 1.4.0 o superior:
... buildscript {
... dependencies {
... classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0"
}
} ...
5. Para Android, tendrás que activar el multiDex, que se encuentra en el archivo de configuración de la aplicación (/android/app/build.gradle por defecto.)
...
android {
... defaultConfig {
... multiDexEnabled true
}
}
¡Voila, ya está todo listo y empieza a codificar!
Recuperar la lista de productos en la aplicación
Hay un montón de cosas útiles que ocurren bajo el capó de react-native-adapty. Seguramente las necesitarás, tarde o temprano, por lo que deberías inicializar la biblioteca al principio de tu flujo. Ve tan alto como puedas en el código de tu aplicación (también puedes hacerlo en el App.tsx) y comienza la inicialización:
// import the method
import { activateAdapty } from 'react-native-adapty';
// We’ve had this App component in our app’s root
const App: React.FC = () => {
...
// we’re invoking it once in a root component on mount
useEffect(() => {
activateAdapty({ sdkKey: 'MY_PUBLIC_KEY' });
},[]);
...
}
Aquí, sustituye MY_PUBLIC_KEY por tu clave de SDK pública que se encuentra en la configuración del dashboard. En realidad, el método activateAdapty() puede ser invocado más de una vez y en más de un lugar, pero nos quedaremos con este diseño.
Ahora, podemos recuperar los productos que hemos añadido en el dashboard de Adapty:
import { adapty } from 'react-native-adapty';
async function getProducts() {
const {paywalls, products} = await adapty.paywalls.getPaywalls();
return products;
}
Ahora, vamos a practicar: Intentaremos compilar una pequeña aplicación en la que podamos navegar por los productos de nuestros muros de pago y realizar compras.
Aplicación de ejemplo
A partir de ahora seré breve para no complicar demasiado la lógica básica. También codificaré en TypeScript para mostrarte qué tipos se utilizan y dónde. Para las pruebas, utilizaré mi viejo iPhone 8. Recuerda que, a partir de iOS 14, la App Store prohíbe el uso de kits de tienda en emuladores: sólo puedes hacer pruebas con dispositivos físicos.
Componente raíz de App.tsx
1. En primer lugar, vamos a crear un componente raíz App.tsx que tendrá un botón de visualización del muro de pago. Ya hemos configurado la navegación a través de react-native-navigation -creemos que es mucho mejor que la opción react-navigation recomendada en los documentos oficiales.
PROBLEMA
import React, { useEffect, useState } from "react";
import { Button, StyleSheet, View } from "react-native";
import { adapty, activateAdapty, AdaptyPaywall } from "react-native-adapty";
export const App: React.FC = () => {
const [paywalls, setPaywalls] = useState<AdaptyPaywall[]>([]);
useEffect(() => {
async function fetchPaywalls(): Promise<void> {
await activateAdapty({ sdkKey: "MY_PUBLIC_KEY" });
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
fetchPaywalls();
}, []);
return (
<View style={styles.container}>
<Button
title="Show the paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such paywall");
}
// Switching to a paywall...
}}
/>
</View>
);
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
});
¿Qué ocurre aquí? Al montar, se invoca la función fetchPaywalls(). Activa el SDK y guarda los muros de pago en el estado para que el usuario no tenga que esperar para obtenerlos después de pulsar el botón. Sólo hay un botón a la vista que debe llevar al usuario al muro de pago que hemos diseñado previamente en el dashboard.
En realidad, es posible obtener los muros de pago aquí mismo, sin guardarlos en el estado. Por defecto, adapty.paywalls.getPaywalls() los obtendrá del almacenamiento de la caché (después de guardarlos en la caché durante el lanzamiento), lo que significa que no tendrás que esperar a que el método se dirija al servidor.
Aquí está el resultado:
Un componente de muro de pago
2. Escribamos un componente de muro de pago en el mismo archivo.
// there are more imports here
import React, { useEffect, useState } from "react";
import {
Button,
SafeAreaView,
StyleSheet,
Text,
View,
PlatformColor,
} from "react-native";
import {
adapty,
activateAdapty,
AdaptyPaywall,
AdaptyProduct,
} from "react-native-adapty";
import { Navigation } from "react-native-navigation";
// ...
interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise<void>;
}
export const Paywall: React.FC<PaywallProps> = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<SafeAreaView style={styles.container}>
{paywall.products.map((product) => (
<View key={product.vendorProductId}>
<Text>{product.localizedTitle}</Text>
<Button
title={`Buy for за ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("Error occured :(");
} finally {
setIsLoading(false);
}
}}
/>
</View>
))}
</SafeAreaView>
);
};
// A new key
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
paywallContainer: {
flex: 1,
alignItems: "center",
justifyContent: "space-evenly",
backgroundColor: PlatformColor("secondarySystemBackground"),
},
});
Aquí, sólo asignaremos los productos del muro de pago y mostraremos un botón de compra junto a cada producto.
Registrar la pantalla
3. Para ver qué aspecto tiene, vamos a registrar esta pantalla en react-native-navigation. Si utilizas otro tipo de navegación, sáltate este paso. Mi archivo index.js raíz tiene este aspecto:
import "react-native-gesture-handler";
import { Navigation } from "react-native-navigation";
import { App, Paywall } from "./App";
Navigation.registerComponent("Home", () => App);
Navigation.registerComponent("Paywall", () => Paywall);
Navigation.events().registerAppLaunchedListener(() => {
Navigation.setRoot({
root: { stack: { children: [{ component: { name: "Home" } }] } },
});
});
Botón «Mostrar el muro de pago»
4. Ahora, sólo tendremos que asignar una acción al botón «Mostrar el muro de pago». En nuestro caso, se mostrará un modal a través de Navigation.
Navigation.showModal<PaywallProps>({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
// Doing everything we need
console.log("purchase", purchase);
},
},
},
});
El archivo App.tsx completo:
import React, { useEffect, useState } from "react";
import {
Button,
SafeAreaView,
StyleSheet,
Text,
View,
PlatformColor,
} from "react-native";
import {
adapty,
activateAdapty,
AdaptyPaywall,
AdaptyProduct,
} from "react-native-adapty";
import { Navigation } from "react-native-navigation";
export const App: React.FC = () => {
const [paywalls, setPaywalls] = useState<AdaptyPaywall[]>([]);
useEffect(() => {
async function fetchPaywalls(): Promise<void> {
await activateAdapty({
sdkKey: "MY_PUBLIC_KEY",
});
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
fetchPaywalls();
}, []);
return (
<View style={styles.container}>
<Button
title="Show paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such paywall");
}
Navigation.showModal<PaywallProps>({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
// Doing everything we need
console.log("purchase", purchase);
},
},
},
});
}}
/>
</View>
);
};
interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise<void>;
}
export const Paywall: React.FC<PaywallProps> = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<SafeAreaView style={styles.paywallContainer}>
{paywall.products.map((product) => (
<View key={product.vendorProductId}>
<Text>{product.localizedTitle}</Text>
<Button
title={`Buy for ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("Error occured :(");
} finally {
setIsLoading(false);
}
}}
/>
</View>
))}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
paywallContainer: {
flex: 1,
alignItems: "center",
justifyContent: "space-evenly",
backgroundColor: PlatformColor("secondarySystemBackground"),
},
});
¡Eso es! Ahora, puedes mostrar estos muros de pago a tus usuarios.
Si quieres probar tu suscripción a iOS en un sandbox, tendrás que crear tu propia cuenta de probador de sandbox. Ten en cuenta que las suscripciones al sandbox se anulan rápidamente para facilitar las pruebas. En el caso de Android, no necesitarás ninguna cuenta adicional, incluso puedes realizar pruebas en un emulador.
Comprueba si el usuario tiene alguna suscripción activa
Todavía tenemos que decidir dónde almacenar los datos de las suscripciones activas para que el usuario final pueda acceder a sus contenidos premium. Adapty también nos ayudará con esto, ya que guarda todas las compras asociadas al usuario. Hagámoslo así: si el usuario no está suscrito, se le mostrará un botón de muro de pago. Si lo tiene, le mostraremos una imagen del gato.
Como los datos de la suscripción activa se recuperan del servidor o del almacenamiento en caché, necesitarás un cargador. Para simplificar, vamos a añadir los estados isLoading y isPremium.
// ...
export const App: React.FC = () => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isPremium, setIsPremium] = useState<boolean>(false);
const [paywalls, setPaywalls] = useState<AdaptyPaywall[]>([]);
useEffect(() => {
async function fetchPaywalls(): Promise<void> {
try {
await activateAdapty({
sdkKey: "MY_PUBLIC_KEY",
});
const profile = await adapty.purchases.getInfo();
const isSubscribed = profile.accessLevels.premium.isActive;
setIsPremium(isSubscribed);
if (!isSubscribed) {
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
} finally {
setIsLoading(false);
}
}
fetchPaywalls();
}, []);
// ...
}
// ...
Esto es lo que ha cambiado: hemos añadido dos marcas al estado. Todo el contenido de fetchPaywalls() está ahora envuelto en un bloque try-catch para que el código llegue a setIsLoading(false) en cualquier escenario posible. Para comprobar si el usuario tiene una suscripción activa, recuperamos el perfil del usuario (que contiene todos sus datos de suscripción) y vemos el valor de profile.accessLevels.premium.isActive. Puedes utilizar tantos niveles de acceso (accessLevels) -que son básicamente niveles de suscripción, como Gold o Premium- como quieras, pero vamos a mantener el valor por defecto por ahora. Adapty creará el nivel de acceso Premium automáticamente, y para la mayoría de las aplicaciones, esto será suficiente. isActive permanecerá verdadero mientras haya una suscripción activa con este nivel de acceso
A partir de aquí, todo parece bastante sencillo. Si el usuario tiene la condición de suscriptor de nivel premium, no hay necesidad de buscar los muros de pago: basta con desactivar el cargador y mostrar el contenido.
export const App: React.FC = () => {
// ...
const renderContent = (): React.ReactNode => {
if (isLoading) {
return <Text>Loading...</Text>;
}
if (isPremium) {
return (
<Image
source={{
url: "https://25.media.tumblr.com/tumblr_lugj06ZSgX1r4xjo2o1_500.gif",
width: Dimensions.get("window").width * 0.8,
height: Dimensions.get("window").height * 0.8,
}}
/>
);
}
return (
<Button
title="Show paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such paywall");
}
Navigation.showModal<PaywallProps>({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
const isSubscribed =
purchase.purchaserInfo.accessLevels?.premium.isActive;
setIsPremium(isSubscribed);
Navigation.dismissAllModals();
},
},
},
});
}}
/>
);
};
return <View style={styles.container}>{renderContent()}</View>;
};
Aquí, estamos añadiendo una función que muestra el contenido, así como algo de lógica a onRequestBuy: concretamente, la actualización del estado de isPremium y el cierre del modal.
Ese es el resultado final:
El archivo completo:
import React, { useEffect, useState } from "react";
import {
Button,
SafeAreaView,
StyleSheet,
Text,
View,
PlatformColor,
Image,
Dimensions,
} from "react-native";
import {
adapty,
activateAdapty,
AdaptyPaywall,
AdaptyProduct,
} from "react-native-adapty";
import { Navigation } from "react-native-navigation";
export const App: React.FC = () => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isPremium, setIsPremium] = useState<boolean>(false);
const [paywalls, setPaywalls] = useState<AdaptyPaywall[]>([]);
useEffect(() => {
async function fetchPaywalls(): Promise<void> {
try {
await activateAdapty({
sdkKey: "MY_PUBLIC_KEY",
});
const profile = await adapty.purchases.getInfo();
const isSubscribed = profile.accessLevels.premium.isActive;
setIsPremium(isSubscribed);
if (!isSubscribed) {
const result = await adapty.paywalls.getPaywalls();
setPaywalls(result.paywalls);
}
} finally {
setIsLoading(false);
}
}
fetchPaywalls();
}, []);
const renderContent = (): React.ReactNode => {
if (isLoading) {
return <Text>Loading...</Text>;
}
if (isPremium) {
return (
<Image
source={{
uri: "https://25.media.tumblr.com/tumblr_lugj06ZSgX1r4xjo2o1_500.gif",
width: Dimensions.get("window").width * 0.8,
height: Dimensions.get("window").height * 0.8,
}}
/>
);
}
return (
<Button
title="Show a paywall"
onPress={() => {
const paywall = paywalls.find(
(paywall) => paywall.developerId === "cats_paywall"
);
if (!paywall) {
return alert("There is no such a paywall");
}
Navigation.showModal<PaywallProps>({
component: {
name: "Paywall",
passProps: {
paywall,
onRequestBuy: async (product) => {
const purchase = await adapty.purchases.makePurchase(product);
const isSubscribed =
purchase.purchaserInfo.accessLevels?.premium.isActive;
setIsPremium(isSubscribed);
Navigation.dismissAllModals();
},
},
},
});
}}
/>
);
};
return <View style={styles.container}>{renderContent()}</View>;
};
interface PaywallProps {
paywall: AdaptyPaywall;
onRequestBuy: (product: AdaptyProduct) => void | Promise<void>;
}
export const Paywall: React.FC<PaywallProps> = ({ paywall, onRequestBuy }) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<SafeAreaView style={styles.paywallContainer}>
{paywall.products.map((product) => (
<View key={product.vendorProductId}>
<Text>{product.localizedTitle}</Text>
<Button
title={`Buy for ${product.localizedPrice}`}
disabled={isLoading}
onPress={async () => {
try {
setIsLoading(true);
await onRequestBuy(product);
} catch (error) {
alert("An error occured :(");
} finally {
setIsLoading(false);
}
}}
/>
</View>
))}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: "center", justifyContent: "center" },
paywallContainer: {
flex: 1,
alignItems: "center",
justifyContent: "space-evenly",
backgroundColor: PlatformColor("secondarySystemBackground"),
},
});
Para resumir todo lo anterior
Hemos acabado compilando una aplicación de suscripción muy bonita y extremadamente útil. Aquellos que paguen verán los gatos, y todos los demás tendrán muros de pago en su lugar. Esta guía debería haberte enseñado todo lo que podrías necesitar para implementar las compras dentro de la aplicación en tu aplicación. Y para aquellos que quieran profundizar en los kits de tienda, permanezcan atentos para saber más. ¡Gracias!