Compras em aplicativos React Native: uma implementação muiti fácil de fazer. Tutorial
Updated: May 23, 2023
25 min read
As estruturas de desenvolvimento de aplicativos (apps) multiplataforma certamente facilitam a vida dos desenvolvedores, permitindo a construção de aplicativos para diversas plataformas simultaneamente. No entanto, há alguns pontos negativos. Por exemplo, o React Native não tem uma ferramenta pronta para implementar compras no aplicativo (in-app purchases). Portanto, você inevitavelmente terá que recorrer a bibliotecas de terceiros.
Que opções existem para a implementação de compras no aplicativo
Bibliotecas populares para assinaturas no aplicativo em aplicativos React Native são a react-native-iap e a expo-in-app-purchases. Mas vou falar sobre a react-native-adapty, porque são muitos os benefícios, em comparação com as outras bibliotecas:
- Ao contrário dessas, a react-native-adapty permite a validação de compra baseada em servidor.
- Ela é compatível com todos os recursos implementados recentemente pelas lojas de aplicativos, desde ofertas promocionais até funcionalidades de pagamento antecipado. Ela também está pronta para suportar novos recursos que venham a surgir.
- O código acaba se tornando mais claro e direto.
- Você pode modificar sua oferta de produtos e adicionar ou remover novas ofertas sem ter que passar por todo o ciclo de entrega. Não é preciso lançar versões beta e esperar pela aprovação.
O SDK da Adapty tem muito mais para oferecer. Você dispõe de ferramentas analíticas integradas para todas as principais métricas, análise de coorte, validação de compra baseada no servidor, testes AB para paywalls, campanhas promocionais com segmentação flexível, integrações de ferramentas analíticas de terceiros e muito mais.
Sobre este artigo
No momento, vamos tratar da configuração de compras no aplicativo no React Native. O que vamos abordar hoje:
- Porque o Expo não funciona com compras no aplicativo no React Native.
- Como criar uma conta de desenvolvedor.
- Como configurar com Adapty:
Configurando a App Store
Configurando a Play Store - Adição de assinaturas.
- Criação de um paywall.
- Instalação de react-native-adapty.
- Um exemplo de aplicativo e o resultado.
Neste guia, vamos tentar construir um aplicativo que exiba fotos de gatos para os usuários assinantes e que induza a todos os outros com uma oferta de assinatura.
Por que o Expo não funciona com compras no aplicativo no React Native
Para resumir uma longa história: O Expo “managed” (geranciado) não é compatível com os métodos nativos que as lojas de aplicativos oferecem para o processamento de compras (também conhecidos como kits de loja). Você precisa se ater ao RN puro ou usar o fluxo de trabalho básico (bare workflow) do Expo.
Sendo bem direto, vou desapontar aqueles que pensaram em usar o Expo: não vai funcionar. O Expo é um framework React Native que facilita e muito o desenvolvimento de aplicativos. No entanto, seu fluxo de trabalho gerenciado não é compatível com o processamento de compras/assinaturas. O Expo não utiliza nenhum código nativo em seus métodos e componentes (os dois são apenas JS), o que é necessário para os kits de loja. Não é possível implementar compras no aplicativo com JavaScript, de modo que você terá que “ejetar”.
Como criar uma conta de desenvolvedor
Primeiro, você precisa criar contas na loja de aplicativos, bem como criar e configurar compras e assinaturas para os sistemas iOS e Android. Esse processo não deve levar mais de 20 minutos.
Caso você ainda não tenha configurado sua conta de desenvolvedor e produtos no App Store Connect e/ou Google Play Console, confira as seguintes orientações:
- Para iOS: leia o guia do início até o título “Como conseguir a lista SKProduct”, pois é onde começamos a discutir as implementações nativas.
- Para Android: leia o guia do início até o título “Como conseguir uma lista de produtos em um aplicativo”.
Como configurar no Adapty
Para o react-native-adapty, é necessário primeiro configurar seu dashboard no Adapty. Este procedimento não leva muito tempo, mas você obterá todas as vantagens listadas acima que o Adapty tem em relação à codificação dura.
Na terceira etapa, você receberá instruções com as configurações da App Store e do Google Play.
Para o sistema iOS, você precisa fazer o seguinte:
- Especificar o ID do pacote;
- Configurar as Notificações do App Store Server;
- Especificar o segredo compartilhado da App Store Connect.
Estes campos são necessários para que as compras funcionem.
Cada campo tem uma dica de “Leia como” que contém orientações passo a passo de como proceder. Verifique-os caso tenha alguma dúvida.
O ID de pacote é como se identifica de maneira única o seu aplicativo. Deve corresponder àquele que você especificou no Xcode, em Targets > [App Name] > General:
No caso do sistema Android, os campos obrigatórios são o Nome do Pacote e o Arquivo de Chave da Conta de Serviço. Todos estes campos têm suas próprias dicas de “Leia como” também. O nome do pacote tem a mesma função no Android que o ID do pacote tem no iOS. Ele deve corresponder ao que você especificou no seu código, que pode ser encontrado no arquivo /android/app/build.gradle em android.defaultConfig.applicationId:
Na quarta etapa, você deverá conectar o SDK da Adapty ao seu aplicativo. Ignore esta etapa por enquanto, mas voltaremos a ele um mais adiante.
Após se inscrever, verifique a guia de configurações e lembre-se de que é aqui que sua chave SDK pública pode ser encontrada. Você vai precisar da chave posteriormente.
Como adicionar uma assinatura
A Adapty utiliza produtos para diferentes assinaturas. Sua assinatura de fotos de gatos pode ser semanal, semestral ou anual. Cada uma destas opções constitui um produto Adapty à parte.
Vamos especificar no dashboard que temos um produto. Para isso, navegue até Produtos & Testes A/B → Produtos e clique em Criar produto.
Agora você precisa especificar o nome do produto, ou seja, como a assinatura aparecerá no seu dashboard da Adapty.
Você também deve especificar o ID do produto na App Store e o ID do produto na Play Store. Caso deseje, especifique o período e o nome também para o processo de analytics. Clique em Salvar.
Como criar um paywall
Em seguida, você deve criar um paywall, que é uma tela que restringe o acesso do usuário a recursos premium e os estimula com uma oferta de assinatura. É necessário adicionar o produto que você criou ao seu paywall. Para isso, clique em “Criar paywall” na mesma seção (Produtos & Testes A/B → Paywalls).
- Escolha um nome para o Paywall que você e sua equipe poderão facilmente inferir, apenas olhando o nome, de qual paywall se trata.
- Você deve usar o ID do paywall para exibir o paywall em questão no seu aplicativo. No caso do nosso aplicativo de exemplo, usaremos “cats_paywall”.
- No menu suspenso “Produto”, selecione sua assinatura.
Clique em Salvar e publicar.
Pronto, a configuração foi concluída. Agora, vamos adicionar as dependências e escrever o código.
Instalação do react-native-adapty
1. Primeiro, adicione a dependência:
yarn add react-native-adapty
2. Instale os pods do iOS. Se você ainda não tem o módulo CLI, recomendo expressamente que você faça o download . Você certamente vai precisar muito dele no desenvolvimento no sistema iOS.
#pods get installed into the native iOS project, which, by default, is the /ios folderpod install --project-directory=ios
3. Como os projetos React Native no iOS são escritos em Obj-C, você deve criar um cabeçalho Swift Bridging para que o Obj-C possa ler as bibliotecas do Swift. Para isso, basta abrir seu projeto Xcode e criar um novo arquivo Swift. O Xcode vai perguntar se você deseja criar um cabeçalho ponte (bridging header), que é exatamente o que você quer. Clique em Criar.
4. No caso do sistema Android, certifique-se de que o project—/android/build.gradle por padrão, esteja usando o plugin kotlin-gradle-plugin da versão 1.4.0 ou superior:
... buildscript {
... dependencies {
... classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0"
}
} ...
5. Para o Android, você deve habilitar o multiDex, que pode ser encontrado no arquivo de configuração do aplicativo (/android/app/build.gradle por padrão.)
...
android {
... defaultConfig {
... multiDexEnabled true
}
}
Voila, você terminou e pode começar a codificar!
Como recuperar a lista de produtos no aplicativo
Há toneladas de coisas úteis acontecendo ao abrigo do react-native-adapty. Você certamente vai precisar delas, mais cedo ou mais tarde, e é por isso que você deve inicializar a biblioteca ainda no início do seu fluxo. Faça o máximo que puder no código do seu aplicativo (você pode fazer isso direto também no App.tsx) e comece a inicialização:
// 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' });
},[]);
...
}
Aqui, substitua MY_PUBLIC_KEY pela sua chave SDK pública encontrada nas configurações do dashboard. Na verdade, o método activateAdapty() pode ser usado mais de uma vez e em mais de um lugar, mas vamos nos ater a este projeto.
Agora, podemos recuperar os produtos que adicionamos no dashboard da Adapty:
import { adapty } from 'react-native-adapty';
async function getProducts() {
const {paywalls, products} = await adapty.paywalls.getPaywalls();
return products;
}
Pronto, vamos começar a praticar: Vamos tentar criar um pequeno aplicativo onde possamos navegar pelos produtos de nossos paywalls e fazer compras.
Exemplo de aplicativo
Vou ser breve a partir de agora, para evitar que a lógica básica se torne complicada demais. Também vou codificar em TypeScript para mostrar quais tipos são usados e onde. Para realizar os testes, vou usar meu bom e velho iPhone 8. Lembre-se que a partir do iOS 14, a App Store proíbe o uso de kits de loja em emuladores – você só pode testar usando dispositivos físicos.
O componente raiz App.tsx
1. Primeiro, vamos criar um componente raiz App.tsx que terá um botão de exibição no paywall. Já configuramos a navegação via react-native-navigation—acreditamos que seja muito melhor do que a opção react-navigation recomendada nos documentos oficiais.
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" },
});
O que está acontecendo? Na montagem, a função fetchPaywalls() foi usada. Ela ativa o SDK e salva os paywalls no estado para que o usuário não tenha que esperar pelo fetchPaywalls depois de tocar o botão. Só existe um botão na visualização que deve levar o usuário até o paywall que projetamos anteriormente no dashboard.
Na verdade, é possível buscar os paywalls aqui mesmo, sem salvá-los no estado. Por padrão, o adapty.paywalls.getPaywalls() irá buscá-los no armazenamento do cache (depois de armazená-los em cache no lançamento), o que significa que você não precisa esperar pelo método para “falar” com o servidor.
O resultado é o seguinte:
Um componente de paywall
2. Vamos escrever um componente de paywall no mesmo arquivo.
// 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"),
},
});
Neste ponto, vamos apenas mapear os produtos a partir do paywall e exibir um botão de compra ao lado de cada produto.
Como registrar a tela
3. Para ver como fica, vamos registrar esta tela no react-native-navigation. Caso esteja usando alguma outra navegação, pule esta etapa. Meu arquivo raiz index.js fica assim:
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ão “Exibir o paywall”
4. Agora, só precisamos atribuir uma ação ao botão “Exibir o paywall”. No nosso caso, ele desencadeará um modal via Navegação.
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);
},
},
},
});
O arquivo completo App.tsx:
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"),
},
});
É isso! Agora, você já pode exibir estes paywalls para seus usuários.
Caso deseje testar sua assinatura do iOS em um Sandbox, você deve criar sua própria conta de testador em ambiente Sandbox. Lembre-se de que as assinaturas de Sandbox são rapidamente invalidadas para simplificar os testes. No caso do sistema Android, você não precisa de nenhuma conta extra – você até pode executar testes em um emulador.
2024 subscription benchmarks and insights
Get your free copy of our latest subscription report to stay ahead in 2024.
Como verificar se o usuário tem alguma assinatura ativa
Ainda precisamos decidir onde armazenar dados de assinatura ativos para conceder ao usuário final acesso ao seu conteúdo premium. A Adapty também nos ajudará nesse sentido, pois ela salva todas as compras associadas ao usuário. Vamos fazer assim: caso o usuário não tenha uma assinatura, ele será avisado com um botão de paywall. Caso tenham, mostraremos a eles uma foto de gato.
Como os dados da assinatura ativa são recuperados do servidor ou do armazenamento no cache, precisaremos de um carregador. Para simplificar, vamos incluir os estados isLoading e 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();
}, []);
// ...
}
// ...
O que mudou foi o seguinte: acrescentamos duas bandeiras ao status. Todo o conteúdo do fetchPaywalls() está agora contido em um bloco de try-catch para que o código chegue ao setIsLoading(false) em qualquer cenário possível. Para verificar se o usuário tem uma assinatura ativa, estamos recuperando o perfil do usuário (que contém todos os dados da assinatura) e verificar o valor do profile.accessLevels.premium.isActive. Você pode usar tantos níveis de acesso (accessLevels)—que são basicamente apenas níveis de assinatura, como Gold ou Premium—quanto desejar, mas vamos usar o valor padrão por enquanto. A Adapty criará o nível de acesso premium automaticamente, e para a maioria das aplicações, será o suficiente. O isActive continuará sendo verdadeiro enquanto houver uma assinatura ativa com este nível de acesso
A partir de agora, tudo vai parecer bastante simples. Caso o usuário tenha o status de assinatura premium, não é necessário buscar os paywalls — basta desativar o carregador e exibir o conteúdo.
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>;
};
Neste ponto, estamos adicionando uma função que renderiza o conteúdo, bem como alguma lógica ao onRequestBuy: isto é, atualizando o estado do isPremium e fechando o modal.
O resultado final é esse:
O arquivo 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
Acabamos criando um aplicativo de assinatura lindo e extremamente útil. Aqueles que pagam verão gatos, e todos os outros, pelo contrário, receberão paywalls. Este guia deveria ter ensinado tudo o que você pode precisar para implementar compras no seu aplicativo. E para aqueles que estão ansiosos para estudar ainda mais os kits de loja, fiquem atentos aos futuros conteúdos. Obrigado!