Ganhar dinheiro
Venda produtos no app (IAP)
Compras do Google Play no seu prĂłprio site ou app em ZIP
1📥 Baixe o guia + app de demonstração funcional#
Tudo neste guia tambĂŠm estĂĄ em um PDF que vocĂŞ pode ler offline, alĂŠm de um app de demonstração completo que vocĂŞ pode compilar e rodar hoje.
Toque abaixo e os dois sĂŁo salvos no seu celular em Downloads/AppMint/Guides:
- AppMint-InApp-Purchases-Guide.pdf - o guia completo, incluindo as etapas do Play Console e os exemplos de React / Next.js
- iap-demo-index.html - um joguinho que vende trĂŞs coisas: remover anĂşncios (compra Ăşnica), um pacote de nĂveis (compra Ăşnica) e um passe de temporada mensal
Para experimentar a demonstração: compacte o iap-demo-index.html sozinho em um ZIP, envie no assistente e siga o guia. Ă a forma mais rĂĄpida de ver uma compra real funcionar de ponta a ponta antes de mexer no seu prĂłprio app.
No app, o botão desta etapa salva esses arquivos no seu celular. Aqui eles são downloads diretos.
AppMint-InApp-Purchases-Guide.pdf iap-demo-index.html2O que ĂŠ isso? (em palavras simples)#
Seu app pode vender coisas (um upgrade Pro, nĂveis extras, uma assinatura) e o Google recebe o dinheiro e paga vocĂŞ todo mĂŞs.
VAMOS CONSTRUIR UM EXEMPLO JUNTOS, ao longo de todo este guia:
A game that sells two things
• 'Levels 11-20' ₹99 paid once, kept forever
• 'Remove ads' ₹49 every month
No fim, vocĂŞ vai adicionar quatro linhas Ă sua pĂĄgina. Esse Ă© o trabalho todo: nĂŁo hĂĄ cĂłdigo de compra para escrever, e mesmo que vocĂŞ nunca tenha feito isso antes, vai dar tudo certo.
VocĂŞ NĂO precisa saber programar alĂŠm de copiar e colar, e NĂO precisa de servidor, banco de dados nem conta em empresa de pagamento. O Google Play cuida das partes difĂceis:
- Mostra a tela de pagamento (cartĂŁo / UPI / cobrança na operadora)
- Converte seu preço para a moeda de cada paĂs
- Lembra quem comprou o quĂŞ, mesmo depois de reinstalar
- Envia o dinheiro para o seu banco
O AppMint fica com 0%: a venda vai direto para a SUA conta do Google Play.
Funciona do mesmo jeito para Site → APK, ZIP → APK e apps criados por IA.
3O que você precisa antes de começar#
Apenas quatro coisas:
- Seu site ou app HTML/ZIP (o que vocĂŞ transforma em APK).
- Algo para vender: decida o que os compradores recebem (por exemplo, 'VersĂŁo Pro': sem limites, conteĂşdo extra).
- Uma conta de desenvolvedor do Google Play: taxa Ăşnica de US$ 25 em play.google.com/console. VocĂŞ precisa dela para publicar QUALQUER app na Play Store, nĂŁo sĂł os pagos.
- Cerca de 1 hora no total, distribuĂdas pelas etapas abaixo.
Importante saber desde jĂĄ: as compras no app sĂł funcionam quando seu app ĂŠ instalado PELA Play Store. Isso ĂŠ uma regra do Google para todos, nĂŁo um limite do AppMint.
4Etapa 1 - Invente o ID do seu produto#
Um ID de produto ĂŠ apenas um nome curto que vocĂŞ inventa para o que vende. VocĂŞ vai digitar esse mesmo nome em dois lugares depois (sua pĂĄgina + Play Console), entĂŁo mantenha simples e anote.
Regras: somente letras minúsculas, números e sublinhados. Deve começar com uma letra ou número.
Copie um destes:
- pro_unlock - upgrade 'VersĂŁo Pro' (um bom padrĂŁo; o cĂłdigo abaixo jĂĄ o usa)
- remove_ads - upgrade sem anĂşncios
- level_pack_2 - pacote de conteĂşdo extra
- coins_100 - pacote de moeda de jogo
- premium_monthly - assinatura mensal
O preço NĂO faz parte do ID: vocĂŞ vai definir o preço na Etapa 5 e pode mudĂĄ-lo a qualquer momento sem mexer no seu cĂłdigo.
5Etapa 2 - Marque seu botĂŁo (sem cĂłdigo para escrever)#
VocĂŞ NĂO escreve nenhum cĂłdigo de compra. Seu app jĂĄ traz o mecanismo de compra: vocĂŞ sĂł marca seu prĂłprio HTML com rĂłtulos para ele saber o que vender.
Copie o bloco abaixo para sua pĂĄgina e altere apenas:
(1) pro_unlock → o ID do seu produto, em data-iap-buy
(2) as palavras no botĂŁo e dentro da seção paga
Ă sĂł isso. O botĂŁo entĂŁo mostra o preço real do Google na moeda do prĂłprio comprador, abre a tela de pagamento do Google ao ser tocado, revela a seção paga depois do pagamento (e de novo a cada abertura para quem jĂĄ comprou), retira o acesso se uma assinatura vencer ou um pagamento for reembolsado, e mostra uma mensagem clara se algo der errado.
OS QUATRO RĂTULOS:
- data-iap-buy="id" - faz deste o botĂŁo de compra (obrigatĂłrio)
- data-iap-label="âŚ{price}âŚ" - texto do botĂŁo; {price} vira o preço real do Google
- data-iap-owned="âŚ" - texto do botĂŁo depois da compra
- data-iap-show - uma seção que aparece somente depois da compra (comece com 'hidden')
TambĂŠm disponĂveis: data-iap-hide (some depois da compra), data-iap-container (um contĂŞiner que sĂł aparece dentro do app), data-iap-text (coloque no <span> que contĂŠm o texto se o botĂŁo tambĂŠm tiver Ăcones).
VENDENDO VĂRIAS COISAS DIFERENTES? Adicione um botĂŁo para cada uma. Elas sĂŁo independentes: um app pode remover anĂşncios E vender pacotes de nĂveis E ter uma assinatura, e comprar um nunca afeta os outros. Aponte uma seção para um produto com data-iap-show="level_pack_2".
Somente os IDs que vocĂŞ digitar em 'Remover anĂşncios (IAP) → ID do produto' desligam os anĂşncios. Todo o resto que vocĂŞ vender libera o que sua pĂĄgina fizer com isso, e os anĂşncios continuam.
DOIS PLANOS PARA A MESMA COISA (mensal e vitalĂcio)? DĂŞ aos dois botĂľes o mesmo data-iap-group="adfree"; assim, comprar qualquer um esconde o outro e ninguĂŠm paga duas vezes. Sem grupo, nada ĂŠ escondido, que ĂŠ o que produtos sem relação precisam.
<!-- (1) CHANGE pro_unlock to your product ID -->
<button data-iap-buy="pro_unlock"
data-iap-label="Unlock Pro — {price}"
data-iap-owned="✓ Unlocked">
Unlock Pro
</button>
<!-- (2) CHANGE the words. Appears only after buying. -->
<div data-iap-show hidden>
🎉 Pro content here — visible after buying.
</div>
<!-- Optional: disappears after buying -->
<div data-iap-hide>
Upgrade to remove the ads.
</div>
<!-- DIFFERENT THINGS: independent — buying one never affects the others -->
<button data-iap-buy="remove_ads" data-iap-label="Remove ads — {price}">Remove ads</button>
<button data-iap-buy="level_pack_2" data-iap-label="Levels 11-20 — {price}">Get levels</button>
<div data-iap-show="level_pack_2" hidden>Levels 11-20 unlocked!</div>
<!-- SAME THING, TWO PLANS: one group, so buying either hides the other -->
<button data-iap-buy="remove_ads_monthly" data-iap-group="adfree"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ Ad-free (monthly)">Remove ads monthly</button>
<button data-iap-buy="remove_ads_lifetime" data-iap-group="adfree"
data-iap-label="Remove ads forever — {price}"
data-iap-owned="✓ Ads removed forever">Remove ads forever</button>6As 4 coisas que as pessoas costumam vender#
Escolha o que combina com vocĂŞ. A Ăşnica diferença entre eles ĂŠ onde vocĂŞ cria o produto no Play Console; o cĂłdigo da sua pĂĄgina tem sempre o mesmo formato.
- REMOVER ANĂNCIOS, PAGO UMA VEZ (para sempre)
Crie em: Produtos no app
Digite tambĂŠm o ID em: Remover anĂşncios (IAP) → ID do produto
- REMOVER ANĂNCIOS, MENSAL
Crie em: Assinaturas
Digite tambĂŠm o ID em: Remover anĂşncios (IAP) → ID do produto
- LIBERAR NĂVEIS / CONTEĂDO, PAGO UMA VEZ
Crie em: Produtos no app
NĂO digite na caixa Remover anĂşncios, senĂŁo seus anĂşncios continuam aparecendo.
- LIBERAR NĂVEIS / CONTEĂDO, MENSAL
Crie em: Assinaturas
NĂO digite na caixa Remover anĂşncios, senĂŁo seus anĂşncios continuam aparecendo.
VocĂŞ pode vender OS QUATRO no mesmo app, e quantos produtos quiser. Comprar um nunca afeta os outros.
<!-- 1. remove ads, paid once -->
<button data-iap-buy="remove_ads_lifetime"
data-iap-label="Remove ads forever — {price}"
data-iap-owned="✓ Ads removed">Remove ads forever</button>
<!-- 2. remove ads, monthly -->
<button data-iap-buy="remove_ads_monthly"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ Ad-free">Remove ads monthly</button>
<!-- 3. levels, paid once -->
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Unlocked">Get levels 11-20</button>
<!-- 4. all levels, monthly -->
<button data-iap-buy="all_levels_monthly"
data-iap-label="All levels — {price}/month"
data-iap-owned="✓ Subscribed">Subscribe</button>7Quais compras removem os anĂşncios?#
Essa ĂŠ a Ăşnica coisa que as pessoas erram, entĂŁo vale 30 segundos.
Seus anĂşncios NĂO fazem parte da sua pĂĄgina web. SĂŁo anĂşncios Android reais colocados por cima dela, entĂŁo nada na sua pĂĄgina consegue desligĂĄ-los. Apenas UMA coisa consegue:
wizard Step 3 → Remove Ads (IAP) → Product ID
Qualquer ID que vocĂŞ digitar nessa caixa remove os anĂşncios quando alguĂŠm o comprar. Todos os outros IDs que vocĂŞ vender apenas liberam o que sua pĂĄgina fizer com eles, e os anĂşncios continuam.
Vendendo um plano sem anĂşncios E pacotes de nĂveis? Digite nessa caixa apenas o ID do plano sem anĂşncios. No nosso exemplo:
Product ID box: remove_ads_monthly
NOT in the box: level_pack_2
Oferecendo dois planos sem anĂşncios (mensal E vitalĂcio)? Coloque os dois nessa mesma caixa, separados por vĂrgula: possuir qualquer um remove os anĂşncios:
remove_ads_monthly,remove_ads_lifetime
NĂŁo vende remoção de anĂşncios? Deixe esse botĂŁo desativado e use apenas '💰 Vender produtos no app'.
Selling levels only:
Remove Ads (IAP) OFF
Sell In-App Products ON
Selling ad removal only:
Remove Ads (IAP) ON → Product ID: remove_ads_monthly
Sell In-App Products ON
Selling both, with two ad-free plans:
Remove Ads (IAP) ON → Product ID: remove_ads_monthly,remove_ads_lifetime
Sell In-App Products ON
(level_pack_2 stays out of the box)8Exemplo completo - pĂĄgina HTML simples#
Aqui estĂĄ nosso app de exemplo completo: vende 'NĂveis 11-20' uma vez e 'Remover anĂşncios' por mĂŞs. Copie, troque os dois IDs e as palavras, e pronto.
Veja o que cada rĂłtulo faz:
- data-iap-buy → o que este botĂŁo vende
- data-iap-label → o texto; {price} vira o preço real do Google
- data-iap-owned → o texto depois que pagaram
- data-iap-show="level_pack_2" → esta caixa aparece somente depois que ESSE produto for comprado
- data-iap-hide → esta caixa some assim que qualquer coisa for comprada
Repare que nĂŁo hĂĄ <script> nem cĂłdigo de compra. Seu app jĂĄ o contĂŠm.
<h2>Get more levels</h2>
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Levels 11-20 unlocked">
Levels 11-20
</button>
<div data-iap-show="level_pack_2" hidden>
🎉 Levels 11 to 20 are yours. Have fun!
</div>
<h2>Tired of ads?</h2>
<button data-iap-buy="remove_ads_monthly"
data-iap-label="Remove ads — {price}/month"
data-iap-owned="✓ No more ads">
Remove ads
</button>
<div data-iap-hide>
Ads help keep this game free.
</div>9Exemplo completo - React ou Next.js#
O JEITO FĂCIL: os mesmos rĂłtulos funcionam em React e Next.js, porque sĂŁo apenas marcação. Sem useState, sem useEffect, sem 'use client'. Botþes que o React adiciona depois sĂŁo detectados automaticamente.
Ă tudo o que a maioria dos apps precisa: use o primeiro bloco abaixo e pare por aĂ.
O JEITO CONTROLADO (segundo bloco): somente se vocĂŞ quiser que o prĂłprio React desenhe o preço e troque a tela. Nesse caso:
- No Next.js, coloque 'use client' na linha 1 do arquivo, ou o build falha.
- SĂł mexa em window.AppMintIAP dentro de useEffect: no corpo do componente ele roda no servidor, onde window nĂŁo existe.
- onChange(fn) entrega o estado na hora e de novo a cada mudança, e retorna a função de cancelar inscrição que o React espera.
- No Next.js, faça o build como exportação estĂĄtica: output: 'export' no next.config.js.
// ── EASY WAY — just JSX, nothing else ──────────────────────────
export default function Shop() {
return (
<>
<button data-iap-buy="level_pack_2"
data-iap-label="Levels 11-20 — {price}"
data-iap-owned="✓ Unlocked">Levels 11-20</button>
<div data-iap-show="level_pack_2" hidden>🎉 Levels 11-20 are yours!</div>
</>
);
}
// ── CONTROLLED WAY — React draws everything ───────────────────
'use client'; // Next.js: must be line 1
import { useEffect, useState } from 'react';
export default function Shop() {
const [iap, setIap] = useState({ available: false, products: {} });
useEffect(() => {
if (typeof window === 'undefined' || !window.AppMintIAP) return;
window.AppMintIAP.start({
plans: [{ id: 'level_pack_2' }, { id: 'remove_ads_monthly' }]
});
return window.AppMintIAP.onChange(setIap);
}, []);
if (!iap.available) return null; // outside the app there is nothing to buy
const levels = iap.products?.level_pack_2; // always optional-chain
return levels?.owned
? <p>🎉 Levels 11-20 are yours!</p>
: <button onClick={() => window.AppMintIAP.buy('level_pack_2')}>
Levels 11-20 {levels?.price}
</button>;
}10Verificação råpida no seu navegador#
Abra sua pĂĄgina em um navegador comum (Chrome no PC ou no celular).
VocĂŞ deve ver o botĂŁo de compra ali parado, sem fazer nada, e sua seção paga ainda escondida. Os dois estĂŁo corretos: fora do app nĂŁo hĂĄ nada para comprar, entĂŁo nada pode ser comprado por acidente.
Dentro do app ĂŠ uma pĂĄgina totalmente diferente: o botĂŁo recebe o preço real, abre a tela de pagamento do Google e se esconde quando o comprador jĂĄ possui o produto. Seu site continua funcionando normalmente para visitantes comuns nos dois casos.
Quer que a oferta desapareça por completo na web? Envolva-a em um <div data-iap-container hidden>: esse contĂŞiner sĂł ĂŠ revelado dentro do app.
11Etapa 3 - Ative as compras no ASSISTENTE#
â ď¸ NĂO PULE ISSO. Se pular, seus botĂľes ainda aparecem, mas cada toque responde 'As compras no app nĂŁo estĂŁo ativadas neste app.' e vocĂŞ nĂŁo recebe dinheiro.
O ASSISTENTE Ă© a sequĂŞncia de telas Etapa 1 → Etapa 2 → Etapa 3 que vocĂŞ usa para criar seu app. As compras ficam na ETAPA 3 DO ASSISTENTE.
FAĂA NESTA ORDEM:
- Abra o assistente e vĂĄ para a ETAPA 3 DO ASSISTENTE (a tela com AnĂşncios, Notificaçþes e Permissþes).
STEPPER STEP 3 — what to set
Ads ......................... ON (needed to see the card below)
💰 Sell In-App Products ..... ON ← REQUIRED for selling anything
Selling ad removal too?
Remove Ads (IAP) ............ ON
Play Store Product ID ....... remove_ads_monthly
(two plans) .......... remove_ads_monthly,remove_ads_lifetime
License Key ................. paste from Play Console
Export ...................... AAB (not APK)12Etapa 4 - Coloque seu app no Google Play#
No seu Play Console (o site onde os desenvolvedores gerenciam seus apps):
- 'Criar app' → preencha o nome e as perguntas bĂĄsicas.
- No menu Ă esquerda: 'Testes' → 'Teste interno' → 'Criar nova versĂŁo' → envie seu arquivo AAB → 'Salvar' e publique.
- Ainda em Teste interno → aba 'Testadores' → adicione seu prĂłprio Gmail → copie o link de 'participação' para usar depois.
O teste interno ĂŠ uma ĂĄrea privada: ninguĂŠm consegue ver seu app ainda. Ele existe para VOCĂ experimentar as compras antes de ir a pĂşblico.
13Etapa 5 - Crie seu produto e defina o preço#
Agora diga ao Google o que vocĂŞ estĂĄ vendendo. O menu que vocĂŞ usa depende de o cliente pagar uma vez ou todo mĂŞs.
PAGO UMA VEZ (nĂveis, Pro, moedas, remover anĂşncios para sempre)
- Play Console → seu app → 'Monetizar' → 'Produtos' → 'Produtos no app'.
- 'Criar produto'.
- ID do produto: digite EXATAMENTE o ID da sua pĂĄgina - level_pack_2. Mesma grafia, mesmas maiĂşsculas e minĂşsculas. O Google nunca permite alterĂĄ-lo nem reutilizĂĄ-lo.
- Nome: o que os compradores veem, por exemplo 'NĂveis 11-20'. Descrição: uma linha.
- Defina seu preço na sua prĂłpria moeda: o Google converte para cada paĂs.
- Clique em 'Ativar'.
TODO MĂS (remover anĂşncios mensal, assinatura de todos os nĂveis)
- Play Console → 'Monetizar' → 'Produtos' → 'Assinaturas'.
- 'Criar assinatura'. ID do produto: remove_ads_monthly.
- Salve e depois adicione um PLANO BASE: tipo 'Renovação automĂĄtica', perĂodo de cobrança 'Mensal', seu preço.
- Clique em 'Ativar' no plano base E na prĂłpria assinatura. Se esquecer qualquer um, seu app nĂŁo consegue vĂŞ-la.
Repita para cada ID que vocĂŞ usou. O cĂłdigo da sua pĂĄgina nĂŁo muda: o app pergunta ao Google de que tipo ĂŠ cada ID.
QUER MENSAL *E* ANUAL? Crie DUAS assinaturas separadas (all_levels_monthly e all_levels_yearly) em vez de dois planos base dentro de uma assinatura. Uma assinatura com vĂĄrios planos base sempre oferece o primeiro, sem como escolher.
Altere qualquer preço depois no Play Console: seu app mostra o novo preço sozinho, sem precisar recompilar.
14Etapa 6 - Teste (vocĂŞ nĂŁo serĂĄ cobrado)#
- Play Console → menu Ă esquerda 'Configuração' → 'Teste de licença' → adicione seu Gmail Ă lista de testadores.
- No celular: abra o link de participação do teste interno da Etapa 4, aceite e instale seu app PELA Play Store.
- Abra o app. Seu botĂŁo de compra aparece com o preço real.
- Toque nele: a tela de pagamento do Google abre e diz 'CartĂŁo de teste, sempre aprova'. Compre. Seu conteĂşdo pro aparece. VocĂŞ nĂŁo paga nada.
- Verificação extra: desinstale e reinstale o app; o conteúdo pro Ê liberado sozinho na abertura. à o Google restaurando a compra.
â ď¸ Se vocĂŞ instalar o APK diretamente (sideload) em vez de pela Play Store, o botĂŁo de compra NĂO vai aparecer. Isso Ă© normal: teste sempre pelo link da Play Store.
15Etapa 7 - Publique e receba#
- Quando os testes estiverem bons: Play Console → 'Produção' → crie uma versĂŁo com o mesmo AAB → publique. (Apps novos passam pela anĂĄlise do Google, geralmente alguns dias.)
- Os usuĂĄrios compram dentro do seu app → o Google recebe o dinheiro.
- O Google paga na sua conta bancĂĄria todo mĂŞs (Play Console → 'Perfil de pagamentos'). O Google fica com a taxa de serviço padrĂŁo (15% para a maioria dos desenvolvedores). O AppMint nĂŁo fica com nada.
Para adicionar mais produtos depois: adicione o novo ID no cĂłdigo da sua pĂĄgina, recompile uma vez e crie o ID no Play Console. Mudanças de preço nunca exigem recompilar.
16Se algo não funcionar#
Seu app diz o que deu errado em vez de falhar em silĂŞncio. Encontre a mensagem que vocĂŞ vĂŞ:
'As compras no app nĂŁo estĂŁo ativadas neste app.'
→ '💰 Vender produtos no app' estava DESATIVADO na ETAPA 3 DO ASSISTENTE quando vocĂŞ compilou. Ative e compile de novo.
'Este item ainda nĂŁo estĂĄ disponĂvel no Google Play.'
→ O Google nĂŁo reconhece esse ID. Confira letra por letra com o Play Console, confirme que o produto estĂĄ ATIVO (em uma assinatura, o plano base E a assinatura devem estar ativados) e verifique se vocĂŞ instalou o app pelo link de testador da Play.
'O Google Play nĂŁo conseguiu iniciar o pagamento.'
→ O app nĂŁo foi instalado pela Play Store, ou foi assinado com uma chave diferente da que vocĂŞ enviou.
'Pagamento pendente - isto serĂĄ liberado automaticamente quando o Google confirmar.'
→ Nada a fazer. MĂŠtodos de pagamento lentos (vales em dinheiro, alguns UPI) levam alguns minutos; a liberação chega sozinha, mesmo depois de reiniciar o app.
NENHUMA MENSAGEM, nada acontece ao tocar
→ Quase sempre ĂŠ um erro de digitação em data-iap-buy, ou o app nĂŁo foi instalado pela Play. Qualquer outra causa mostra uma das mensagens acima.
O PAGAMENTO FUNCIONOU, MAS OS ANĂNCIOS CONTINUAM
→ Esse ID de produto nĂŁo estĂĄ na ETAPA 3 DO ASSISTENTE → 'Remover anĂşncios (IAP)' → 'ID do produto na Play Store'. Adicione-o (separe vĂĄrios por vĂrgula) e compile de novo.
O BOTĂO DE COMPRA NUNCA APARECE NO APP
→ '💰 Vender produtos no app' estava desativado na hora do build. Em um navegador comum, os botĂľes ficam escondidos DE PROPĂSITO: nĂŁo hĂĄ nada para comprar fora do app.
NADA ACONTECEU DEPOIS QUE FECHEI A TELA DE PAGAMENTO DO GOOGLE
→ Isso ĂŠ um cancelamento normal. Toque de novo e conclua o pagamento.
PREĂO PARECE ANTIGO
→ A Play guarda os preços em cache no dispositivo por algumas horas. Uma instalação nova sempre mostra o preço novo.
17React, Next.js ou JavaScript puro#
REACT E NEXT.JS NĂO PRECISAM DE NADA ESPECIAL. Os rĂłtulos acima sĂŁo apenas marcação, entĂŁo a mesma linha Ăşnica funciona lĂĄ tambĂŠm: sem useState, sem useEffect, sem 'use client':
<button data-iap-buy="pro_unlock"
data-iap-label="Unlock Pro — {price}">Unlock Pro</button>
Botþes que o React monta depois sĂŁo encontrados automaticamente, e remontar Ă© seguro. No Next.js, faça o build como exportação estĂĄtica (output: 'export').
Prefere cĂłdigo a rĂłtulos? Uma linha faz o mesmo trabalho:
AppMintIAP.sell('pro_unlock', '#buy-btn');
Terceiro argumento opcional: {label:'Desbloquear Pro - {price}', owned:'â Desbloqueado', show:'#thanks', hide:'.upsell'}. Chame uma vez por produto.
SOMENTE SE VOCĂ QUISER QUE O REACT DESENHE O PREĂO SOZINHO; caso contrĂĄrio, pule isto:
const [iap, setIap] = useState({ available: false, products: {} });
useEffect(() => {
if (typeof window === 'undefined' || !window.AppMintIAP) return;
window.AppMintIAP.start({ plans: [{ id: 'pro_unlock' }] });
return window.AppMintIAP.onChange(setIap);
}, []);
const pro = iap.products?.pro_unlock; // sempre use encadeamento opcional
if (!iap.available) return null; // nada para comprar fora do app
if (pro?.owned) return <p>Obrigado!</p>;
return <button onClick={() => window.AppMintIAP.buy('pro_unlock')}>
Unlock Pro {pro?.price}
</button>;
DOIS HĂBITOS EVITAM QUE ISSO QUEBRE:
- Leia a posse POR PRODUTO - iap.products?.<id>?.owned. O iap.owned de nĂvel superior significa 'qualquer coisa foi adquirida' e muda de significado no dia em que vocĂŞ vender um segundo produto.
- Use encadeamento opcional em todo o caminho, para que um campo inesperado nunca cause erro enquanto o React renderiza.
onChange entrega {available, adFree, owned, ownedIds, products} imediatamente e a cada mudança, e retorna uma função para cancelar a inscrição, entĂŁo quem jĂĄ comprou antes jĂĄ aparece liberado na primeira renderização. No Next.js, adicione 'use client' na linha 1 e faça o build com output: 'export'.
TambĂŠm: AppMintIAP.available, .isOwned(id), .owned(), .buy(id), .state(), .refresh(). Os rĂłtulos data-iap-buy funcionam tambĂŠm em JSX, e botĂľes que o React monta depois sĂŁo detectados automaticamente.
Prefere controlar a bridge pura vocĂŞ mesmo? Tudo o que estĂĄ acima ĂŠ construĂdo sobre isto:
- iap.isOwned(id) - verdadeiro/falso instantâneo, funciona offline
- iap.getOwnedProducts() - array JSON com todos os IDs adquiridos
- iap.getProducts(reqId, idsJson) - preços ao vivo; o resultado chega no evento
'appmint:products'{requestId, products:[{productId, title, price, currency, type, owned}]} - iap.purchase(id) - produtos de compra Ăşnica E assinaturas (detectados automaticamente)
- Eventos:
'appmint:purchase'{productId} (libere SOMENTE aqui; um toque em algo jĂĄ adquirido dispara de novo),'appmint:owned-changed'{products} (a restauração na abertura terminou, ou um reembolso retirou algo: leia a posse de novo),'appmint:purchase-failed'{reason: disabled | not_found | billing_error | pending}
Vendendo apenas um upgrade de 'remover anĂşncios'? Use o par integrado, mais simples: ative 'Remover anĂşncios (IAP)' na Etapa 3 do assistente:
var iap = window.WebToApk;
if (iap && !iap.isPremium()) {
showRemoveAdsButton(function onTap() {
iap.startRemoveAdsPurchase();
});
}
window.addEventListener('appmint:premium', function (e) {
if (e.detail.premium) hideRemoveAdsButton();
});Remover anĂşncios (IAP)
Configuração do Google Play Billing
1Criar produto gerenciado#
No painel do seu Google Play Console:
- VĂĄ em 'Monetização' > 'Produtos no app'
- Clique em 'Criar produto'
- Digite um ID de produto (por exemplo, 'remove_ads_premium')
- Defina um Nome (por exemplo, 'Remover todos os anĂşncios') e uma Descrição
- Defina o Preço e clique em 'Salvar'
- Clique em 'Ativar' para disponibilizĂĄ-lo para testes
Vendendo a remoção de anĂşncios como ASSINATURA? Crie em 'Monetização' > 'Assinaturas' com um plano base de renovação automĂĄtica e ative TANTO o plano base quanto a assinatura. O modo sem anĂşncios dura enquanto a assinatura estiver ativa, e os anĂşncios voltam sozinhos quando ela vence.
2Obtenha sua chave de licença#
O Google Play exige uma chave pública RSA para verificar as compras com segurança.
No Play Console:
- VĂĄ em 'Monetização' > 'Configuração de monetização'
- Em 'Licenciamento', copie a 'Chave pĂşblica RSA codificada em Base64'
- â ď¸ IMPORTANTE: vocĂŞ vai precisar colar isso no Gerador mais tarde.
Example Key Format:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...3Configuração no Gerador do WebToApk#
No app gerador:
- Ative a chave 'Remover anĂşncios (IAP)' na Etapa 3
- Digite exatamente o mesmo 'ID do produto' que vocĂŞ criou na Etapa 1
- Cole sua 'Chave de licença Base64' da Etapa 2
- Gere seu APK/AAB
Oferecendo mais de um plano sem anĂşncios (por exemplo, uma assinatura mensal E um desbloqueio vitalĂcio de compra Ăşnica)? Coloque todos os IDs de produto neste Ăşnico campo, separados por vĂrgulas: possuir QUALQUER um deles remove os anĂşncios. O primeiro ID listado ĂŠ o que o item de menu integrado 'Remover anĂşncios' compra, entĂŁo coloque seu plano padrĂŁo primeiro.
Product ID: remove_ads_premium
Two plans:
remove_ads_monthly,remove_ads_lifetime4Configure os testadores de licença#
Para testar compras sem gastar dinheiro de verdade:
- No Play Console, vĂĄ em 'Configuração' > 'Teste de licença'
- Adicione seu endereço de Gmail Ă lista de 'Testadores de licença'
- Confirme que a 'Resposta de licença' estĂĄ definida como 'RESPOND_NORMALLY'
5Teste na faixa Interna ou Fechada#
As compras no app SĂ funcionam quando o app ĂŠ instalado pela Play Store (faixas de teste):
- Envie o AAB gerado para a faixa de 'Teste interno' ou 'Teste fechado'
- Compartilhe o link de participação de testadores com suas contas de teste
- Baixe o app na Play Store
- Toque no botĂŁo 'Remover anĂşncios' no seu app para testar o fluxo!
Note: Purchases will show a 'Test instrument, always approves' message for license testers.Receitas de cĂłdigo
As etapas acima cobrem o bĂĄsico: os quatro rĂłtulos, uma pĂĄgina simples completa, a linha Ăşnica do React. Estas receitas cobrem o que elas nĂŁo cobrem: vĂĄrios planos que se substituem, assinaturas que vencem, hooks de frameworks, a bridge pura completa e as tabelas de referĂŞncia.
A regra Ăşnica#
Uma compra ĂŠ liberada somente quando o Google a confirma. NĂŁo quando o botĂŁo ĂŠ clicado, nĂŁo quando a tela abre, nĂŁo quando o usuĂĄrio volta Ă sua pĂĄgina. Tudo abaixo ĂŠ uma forma diferente de expressar essa Ăşnica regra.
O runtime jĂĄ garante isso: AppMintIAP relĂŞ a posse a partir da bridge na abertura, a cada compra, a cada reembolso e toda vez que o app ĂŠ reaberto. A posse sobrevive a reinstalaçþes, a um celular novo e a ficar offline, porque fica na conta Google do comprador, nĂŁo na sua pĂĄgina.
Nunca libere no tratador de clique e nunca guarde o direito de acesso apenas em localStorage. Limpar os dados do navegador nĂŁo pode fazer um cliente que pagou perder o que comprou, e copiar um valor de localStorage nĂŁo pode liberar o produto de graça.
Receita 1 - uma tabela de preços com valores ao vivo#
TrĂŞs planos em que comprar qualquer um substitui os outros. group ĂŠ o que faz deles alternativas: sem isso, sĂŁo produtos sem relação e comprar o plano vitalĂcio deixaria o botĂŁo mensal ali parado.
<div class="plans" data-iap-container>
<button data-iap-buy="pass_monthly" data-iap-group="pass"
data-iap-label="Monthly — {price}" data-iap-owned="Active">Monthly</button>
<button data-iap-buy="pass_yearly" data-iap-group="pass"
data-iap-label="Yearly — {price}" data-iap-owned="Active">Yearly</button>
<button data-iap-buy="pass_lifetime" data-iap-group="pass"
data-iap-label="Lifetime — {price}" data-iap-owned="Owned">Lifetime</button>
</div>
<p data-iap-show="pass">You are a member. Thank you.</p>
<p data-iap-hide="pass">Members get every level and no ads.</p>Um ID de produto que o Google nĂŁo conhece (ainda nĂŁo criado, nĂŁo ativado, ou o app nĂŁo foi instalado pela Play Store) simplesmente fica sem preço. O botĂŁo continua tocĂĄvel de propĂłsito: ao tocar, ele informa o motivo real em vez de nĂŁo fazer nada.
Receita 2 - React#
onChange dispara imediatamente com o estado atual e de novo a cada mudança, e retorna sua prĂłpria função para cancelar a inscrição, ou seja, tem exatamente o formato que um useEffect espera.
import { useEffect, useState } from 'react';
/** Purchase state for the whole app. Safe in a browser too: available is false there. */
export function useIap() {
const [iap, setIap] = useState({ available: false, ownedIds: [], products: {}, adFree: false });
useEffect(() => {
if (!window.AppMintIAP) return; // running in a browser, or IAP not enabled
return window.AppMintIAP.onChange(setIap); // returns the unsubscribe fn
}, []);
return {
...iap,
owns: (id) => iap.ownedIds.includes(id),
priceOf: (id) => iap.products[id]?.price ?? null,
buy: (id) => window.AppMintIAP?.buy(id)
};
}import { useIap } from './hooks/useIap';
export function ProGate({ children }) {
const { available, owns, priceOf, buy } = useIap();
if (owns('pro_unlock')) return children;
return (
<div className="upsell">
<h2>Pro</h2>
<p>Unlimited projects, no watermark.</p>
<button onClick={() => buy('pro_unlock')} disabled={!available}>
{available ? `Unlock Pro${priceOf('pro_unlock') ? ` — ${priceOf('pro_unlock')}` : ''}` : 'Not available here'}
</button>
</div>
);
}O botĂŁo chama buy(); ele nĂŁo libera nada. A liberação acontece porque o Google confirma, onChange dispara, owns('pro_unlock') vira verdadeiro e o componente renderiza de novo. Ă a mesma regra, expressa em React.
Receita 3 - Vue#
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
const iap = ref({ available: false, ownedIds: [], products: {} });
let stop;
onMounted(() => { stop = window.AppMintIAP?.onChange(s => (iap.value = s)); });
onUnmounted(() => stop && stop());
const isPro = computed(() => iap.value.ownedIds.includes('pro_unlock'));
const price = computed(() => iap.value.products.pro_unlock?.price ?? '');
</script>
<template>
<slot v-if="isPro" />
<button v-else :disabled="!iap.available" @click="AppMintIAP.buy('pro_unlock')">
Unlock Pro {{ price }}
</button>
</template>Receita 4 - assinaturas#
Uma assinatura ĂŠ comprada exatamente como um produto de compra Ăşnica; a diferença ĂŠ que ela pode deixar de valer. type diz qual tipo de produto ĂŠ e period informa o intervalo de cobrança como uma duração ISO-8601: P1M mensal, P1Y anual, P1W semanal.
AppMintIAP.onChange(state => {
const pass = state.products.season_pass; // undefined until Google answers
const active = state.ownedIds.includes('season_pass');
document.body.classList.toggle('has-pass', active);
if (pass && !active) {
const every = { P1W: 'week', P1M: 'month', P1Y: 'year' }[pass.period] || 'period';
label.textContent = `Season pass — ${pass.price} / ${every}`;
}
});
// The pass lapsed (not renewed, or refunded): the same handler runs with it gone.
// Nothing to write — the class comes off and the paid content hides itself.VocĂŞ nĂŁo faz polling e nĂŁo precisa de data de expiração. A Play restaura o conjunto de produtos adquiridos a cada abertura, entĂŁo uma assinatura vencida simplesmente deixa de aparecer em ownedIds.
Ative tanto a assinatura quanto o plano base dela no Play Console. Uma assinatura com plano base inativo nĂŁo retorna nenhum detalhe, e o botĂŁo vai informar "ainda nĂŁo disponĂvel no Google Play".
Receita 5 - a bridge pura, sem helper#
Tudo o que estĂĄ acima Ă© construĂdo sobre seis mĂŠtodos e cinco eventos. Se vocĂŞ quiser controlar a interface por completo, use-os diretamente.
const bridge = window.WebToApk;
const enabled = !!(bridge && bridge.purchase); // false in a browser
// --- what does Google charge for these, in this buyer's currency? ---
const reqId = 'prices-' + Date.now();
window.addEventListener('appmint:products', function onProducts(e) {
if (e.detail.requestId !== reqId) return; // not our answer
window.removeEventListener('appmint:products', onProducts);
for (const p of e.detail.products) {
// {productId, type:'inapp'|'subs', title, description, price, currency, period?, owned}
render(p);
}
});
bridge.getProducts(reqId, JSON.stringify(['pro_unlock', 'season_pass']));
// --- what does this buyer already own? instant, and works offline ---
const owned = JSON.parse(bridge.getOwnedProducts() || '[]');
const isPro = bridge.isOwned('pro_unlock');
// --- buy ---
buyButton.onclick = () => bridge.purchase('pro_unlock');
// --- the ONLY place a purchase is ever granted ---
window.addEventListener('appmint:purchase', e => {
if (e.detail.productId === 'pro_unlock' && e.detail.owned) unlockPro();
});
// --- and the only place failures show up ---
window.addEventListener('appmint:purchase-failed', e => {
toast({
not_found: 'This item is not available on Google Play yet.',
billing_error: 'Google Play could not start the payment. Please try again.',
pending: 'Payment pending — this unlocks automatically once Google confirms.',
disabled: 'In-app purchases are not enabled in this app.'
}[e.detail.reason] || 'Purchase could not be completed. Please try again.');
});
// --- the launch-time restore finished, or a refund took something away ---
window.addEventListener('appmint:owned-changed', () => refreshEverything());appmint:purchase dispara de novo para algo que jĂĄ ĂŠ do usuĂĄrio, de propĂłsito. Escreva unlockPro() de modo que executĂĄ-la duas vezes seja inofensivo e vocĂŞ nunca precise controlar se jĂĄ a executou.
Receita 6 - degrade com honestidade fora do app#
A mesma pĂĄgina normalmente tambĂŠm precisa abrir em um navegador comum, durante o desenvolvimento ou porque vocĂŞ tambĂŠm a publica na web. LĂĄ, window.WebToApk nĂŁo existe e nenhuma compra pode acontecer. Diga isso, em vez de mostrar um botĂŁo que nĂŁo faz nada.
const inApp = !!(window.AppMintIAP && window.AppMintIAP.available);
if (!inApp) {
buyButton.disabled = true;
buyButton.textContent = 'Available in the Android app';
// and let people get it:
storeLink.hidden = false;
}Referência#
Tudo o que a pĂĄgina pode chamar, escutar ou marcar.
| Chamada | O que faz |
|---|---|
AppMintIAP.available | Falso em um navegador, ou quando as compras no app nĂŁo foram ativadas neste build. Verifique antes de mostrar qualquer coisa Ă venda. |
AppMintIAP.sell(id, button, opts) | Conecta um produto a um botĂŁo. opts: label, owned, group, show, hide, labelEl. Chame uma vez por produto. |
AppMintIAP.start(config) | Conecte vĂĄrios de uma vez: {plans, show, hide, container, toast, hideOtherPlans}. Use quando quiser sua prĂłpria função de toast, ou para manter visĂveis os outros planos de um grupo depois que um for comprado (hideOtherPlans: false). |
AppMintIAP.onChange(fn) | Dispara imediatamente com o estado atual e depois a cada mudança. Retorna uma função para cancelar a inscrição. |
AppMintIAP.state() | O mesmo objeto, uma vez: {available, adFree, owned, ownedIds, products}. |
AppMintIAP.isOwned(id) | Verdadeiro/falso, instantâneo, offline. |
AppMintIAP.owned() | Array com todos os IDs de produto adquiridos. |
AppMintIAP.buy(id) | Abre a tela de compra do Google. Sozinho, nĂŁo libera nada. |
AppMintIAP.refresh() | Verifica de novo os botĂľes data-iap-buy adicionados desde a Ăşltima varredura. Raramente necessĂĄrio: o runtime observa o DOM. |
| Chamada | Retorna |
|---|---|
purchase(productId) | Nada. Abre a tela; o resultado chega como um evento. |
getProducts(requestId, idsJson) | Nada. A resposta chega como appmint:products carregando o mesmo requestId. |
getOwnedProducts() | Array JSON com os IDs de produto adquiridos, como string. |
isOwned(productId) | Booleano: um produto de compra Ăşnica adquirido, ou uma assinatura ativa no momento. |
isPremium() | Booleano: especificamente o direito de acesso de "remover anĂşncios". |
startRemoveAdsPurchase() | Nada. Abre a tela para o PRIMEIRO ID do campo "Remover anĂşncios" do assistente; possuir qualquer ID listado ali remove os anĂşncios. O resultado chega como appmint:premium. |
| Evento | detail | Quando |
|---|---|---|
appmint:purchase | {productId, owned} | O Google confirmou. O Ăşnico lugar para liberar. Dispara de novo para algo jĂĄ adquirido. |
appmint:purchase-failed | {productId, reason} | A tela nunca abriu, o fluxo deu erro ou o pagamento estĂĄ pendente. Nunca dispara em um simples cancelamento. |
appmint:products | {requestId, products[]} | Resposta a uma chamada getProducts(). IDs que a Play nĂŁo conhece simplesmente nĂŁo aparecem. |
appmint:premium | {premium} | O direito de acesso sem anĂşncios mudou: compra, restauração na abertura, reembolso ou expiração. |
appmint:owned-changed | {products[]} | A restauração na abertura terminou, ou um reembolso retirou algo. Traz todos os IDs adquiridos. |
| Atributo | Ativado | O que faz |
|---|---|---|
data-iap-buy="id" | botĂŁo | Faz com que ele compre esse produto. |
data-iap-label="… {price}" | botĂŁo | Modelo de rĂłtulo. {price} vira o preço local atual do Google. |
data-iap-owned="…" | botĂŁo | RĂłtulo depois que o produto for adquirido. |
data-iap-group="name" | botĂŁo | Marca planos como alternativas entre si. |
data-iap-show="id|group" | qualquer coisa | Mostrado somente quando o produto foi adquirido. |
data-iap-hide="id|group" | qualquer coisa | Escondido quando o produto foi adquirido. |
data-iap-text | dentro de um botĂŁo | O elemento cujo texto recebe o rĂłtulo, quando o botĂŁo tem mais coisas dentro do que sĂł texto. |
data-iap-container | contĂŞiner | Mostrado somente dentro do app; escondido no navegador, onde nĂŁo hĂĄ nada para comprar. Envolva a oferta inteira nele. |
data-iap-state="owned" | definido pelo runtime | No botĂŁo de um produto adquirido, para vocĂŞ poder estilizĂĄ-lo. |
| reason | O que realmente aconteceu |
|---|---|
not_found | O Google nĂŁo conhece esse ID de produto: ele nĂŁo foi criado, nĂŁo foi ativado, ou o app nĂŁo foi instalado pela Play Store. |
billing_error | A Play nĂŁo conseguiu iniciar o pagamento. Normalmente ĂŠ passageiro; o comprador deve tentar de novo. |
pending | Um mĂŠtodo de pagamento lento (dinheiro, transferĂŞncia bancĂĄria). Libera sozinho quando o Google confirma; nĂŁo trate isso como falha. |
disabled | As compras no app nĂŁo foram ativadas neste build. |
unknown | Qualquer outra coisa. |
Antes de publicar#
- Todos os IDs de produto do seu cĂłdigo existem no Play Console e estĂŁo ativados; no caso de uma assinatura, o plano base tambĂŠm.
- O build leva os mesmos IDs e a chave de licença RSA em Base64 da configuração de monetização.
- Seu endereço de Gmail estĂĄ em Configuração → Teste de licença, com a resposta definida como RESPOND_NORMALLY.
- VocĂŞ enviou o AAB para o Teste interno ou fechado e o instalou pelo link da Play Store; um APK instalado manualmente nunca consegue comprar nada.
- VocĂŞ comprou cada produto como testador de licença e viu "Instrumento de teste, sempre aprova".
- VocĂŞ desinstalou e reinstalou, e a compra voltou sozinha.
- VocĂŞ desligou a rede e o conteĂşdo pago continua liberado.
- VocĂŞ reembolsou uma compra de teste no Play Console e viu o app bloquear de novo.
Erros que custam dinheiro#
- Liberar no tratador de clique. Qualquer pessoa que abre a tela e desiste leva o produto de graça.
- Guardar o direito de acesso somente em
localStorage. Limpar os dados do site tira algo que o cliente pagou; copiar um valor libera de graça. - Fixar o preço no seu HTML. Ele estarĂĄ errado em todos os outros paĂses e desatualizado no dia em que vocĂŞ mudĂĄ-lo. Use
{price}ouproducts[id].price. - Vender bens fĂsicos ou um serviço do mundo real pelo faturamento da Play. O Google exige o contrĂĄrio: isso deve usar um processador de pagamento comum. Ă para isso que serve o guia do Stripe.
- Testar em um APK instalado manualmente. As compras no app sĂł funcionam quando o app foi instalado pela Play Store. Nada que vocĂŞ escreva muda isso.
- Tratar
pendingcomo uma falha. O comprador pagou por um mĂŠtodo lento; o produto ĂŠ liberado sozinho quando o Google confirma.