Como adicionar compras no aplicativo (in-app purchases) a um app Flutter
Adicione compras no app a um aplicativo Flutter com o plugin in_app_purchase. Configure as lojas Apple e Google, valide transações, restaure compras e teste com segurança.
Um app Flutter que vende bens digitais ou assinaturas no iOS ou no Android geralmente precisa receber os pagamentos pelo sistema de compras no aplicativo da Apple ou pelo Google Play Billing. Stripe e checkouts personalizados não são permitidos nesse caso, e ambas as lojas cobram uma comissão sobre cada venda.
Se o seu app já recebe pagamentos via Stripe na web, você pode esperar reaproveitar esse checkout na versão mobile. No caso de bens digitais, esse plano costuma terminar em rejeição pelo App Review.
A comissão padrão da Apple é de 30%. Pelo App Store Small Business Program, desenvolvedores que faturaram até 1 milhão de dólares em receita no ano anterior pagam 15%, tanto em apps pagos quanto em compras no aplicativo. O Google não tem uma taxa única, e sua página de taxas de serviço informa que 99% dos desenvolvedores que pagam alguma taxa se qualificam para 15% ou menos. O Stripe continua sendo a ferramenta certa para bens físicos e para serviços usados fora do app. As regras estão descritas nas Diretrizes de Revisão da App Store sobre compras no aplicativo, da Apple, e na política de Pagamentos do Google Play. Na União Europeia, o Digital Markets Act (DMA) exige que as lojas permitam opções de pagamento alternativas, e a Apple descreve seus mecanismos para a UE em sua página para desenvolvedores sobre o DMA. Na vitrine dos Estados Unidos, as diretrizes da Apple agora também permitem que apps incluam botões e links para opções de compra externas. As regras sobre links de pagamento externos variam por região e mudam com frequência, então leia as diretrizes atuais das duas lojas antes de projetar o seu fluxo.
Este artigo aborda o plugin oficial in_app_purchase do início ao fim: tipos de produto, configuração das lojas, uma classe de serviço funcional, os modos de falha que fazem você perder compras e os testes. Se você está começando com Flutter, comece pelo guia do Flutter para novos desenvolvedores.
Principais pontos
- Bens digitais vendidos dentro de apps iOS e Android geralmente precisam passar pelos sistemas de cobrança da Apple e do Google. O Stripe atende bens físicos e serviços usados fora do app.
buyConsumableebuyNonConsumableretornam umFuture<bool>que apenas indica que a solicitação foi iniciada. O resultado real chega comoPurchaseDetailsnopurchaseStream.- Se
completePurchasenão for chamado em uma compra cujopendingCompletePurchaseseja true, o Google Play a reembolsa após três dias e a App Store deixa a transação não finalizada. - As diretrizes de revisão da Apple exigem um mecanismo de restauração para compras restauráveis.
- Só libere conteúdo depois que o seu backend validar o
serverVerificationDatacom a App Store Server API ou a Google Play Developer API.
Consumíveis, não consumíveis e assinaturas
O plugin in_app_purchase do Flutter suporta três tipos de produto: consumíveis, não consumíveis e assinaturas. Consumíveis usam buyConsumable. Não consumíveis e assinaturas usam buyNonConsumable. O tipo também determina o que o seu app precisa registrar.
| Tipo | Método de compra | Exemplo | O que o app precisa registrar | Restaurável? |
|---|---|---|---|---|
| Consumível | buyConsumable | 100 moedas | Saldo, armazenado no seu backend | Não |
| Não consumível | buyNonConsumable | Desbloqueio Premium | Flag de direito de acesso (entitlement) | Sim |
| Assinatura | buyNonConsumable | Pro Mensal | Data de expiração e estado de renovação, a partir da verificação no servidor | Sim |
Os consumíveis são o único tipo que a loja não registra por você. Depois que um pacote de moedas é consumido, o seu próprio backend mantém o único registro do saldo. Um direito de acesso não consumível sempre pode ser reconstruído com uma restauração. O estado de uma assinatura muda ao longo do tempo por meio de renovações, expirações e reembolsos, então leia-o a partir de dados verificados no servidor, e não de uma flag local em cache.
A configuração das lojas é a maior parte do trabalho
A maior parte do esforço para adicionar compras no aplicativo em Flutter está no App Store Connect e no Play Console. O lado Dart é curto. Enquanto as duas lojas não estiverem configuradas, queryProductDetails não retornará nada, por mais correto que seu código esteja.
Apple (App Store Connect):
- Assine o contrato de Apps Pagos (Paid Apps Agreement) e preencha os dados bancários e fiscais.
- Crie cada compra no aplicativo com um ID de produto como
premium_unlock. Crie as assinaturas dentro de um grupo de assinaturas. - Preencha os metadados para que cada produto chegue ao status Ready to Submit (Pronto para envio).
Google (Play Console):
- Envie um build para uma faixa de teste. A faixa interna funciona.
- Crie os produtos. Use os mesmos IDs da Apple ou faça o mapeamento no código.
- Ative-os. O Google observa que os produtos precisam estar publicados antes que alguém possa testá-los.
Se queryProductDetails listar seus IDs em notFoundIDs, a causa quase sempre está na configuração da loja, e não no Dart. As causas típicas são um ID divergente, um produto inativo, um contrato de Apps Pagos não assinado ou nenhum build enviado para uma faixa de teste do Play.
Código de compras no aplicativo em Flutter: stream, compra, conclusão
O fluxo de compras no aplicativo em Flutter tem quatro etapas. Inscreva-se no purchaseStream na inicialização, verifique isAvailable, carregue os produtos com queryProductDetails e então chame um método de compra. Trate todos os resultados no listener do stream.
Adicione a dependência:
dependencies:
in_app_purchase: ^3.3.1
O pacote in_app_purchase suporta Android SDK 24+, iOS 13+ e macOS 10.15+. As implementações de StoreKit e Play Billing são pacotes federados endossados, incluídos automaticamente, então você só precisa adicioná-los manualmente se usar suas classes específicas de plataforma. Nas plataformas da Apple, o StoreKit 2 é o padrão nos dispositivos que o suportam.
buyConsumable e buyNonConsumable não retornam uma compra. Um erro comum é este:
// Wrong: the Future resolves to a bool, not a purchase
final result = await iap.buyNonConsumable(purchaseParam: param);
if (result) unlockPremium(); // true only means the sheet opened
A versão correta trata o bool como “solicitação iniciada” e faz toda a concessão de acesso no listener do stream:
final started = await iap.buyNonConsumable(purchaseParam: param);
if (!started) showError('Could not start purchase');
// Unlocking happens in the purchaseStream handler
Aqui está o serviço completo, inscrito em main() antes de runApp. O README do plugin recomenda fazer a inscrição no initState do seu app. Fazê-la em main() antecipa esse passo, garantindo que exista um listener antes que o plugin possa emitir qualquer atualização.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
class PurchaseService {
PurchaseService._();
static final instance = PurchaseService._();
static const coins100 = 'coins_100';
static const premium = 'premium_unlock';
final InAppPurchase _iap = InAppPurchase.instance;
StreamSubscription<List<PurchaseDetails>>? _sub;
void start() {
_sub ??= _iap.purchaseStream.listen(
_onPurchases,
onDone: () => _sub?.cancel(),
onError: (Object e) {/* log */},
);
}
Future<List<ProductDetails>> loadProducts() async {
if (!await _iap.isAvailable()) return [];
final res = await _iap.queryProductDetails({coins100, premium});
if (res.notFoundIDs.isNotEmpty) {/* store setup problem */}
return res.productDetails;
}
Future<bool> buy(ProductDetails p) {
final param = PurchaseParam(productDetails: p);
return p.id == coins100
? _iap.buyConsumable(purchaseParam: param)
: _iap.buyNonConsumable(purchaseParam: param);
}
Future<void> restore() => _iap.restorePurchases();
Future<void> _onPurchases(List<PurchaseDetails> purchases) async {
for (final p in purchases) {
var finish = true;
switch (p.status) {
case PurchaseStatus.pending:
// show "waiting for approval"
break;
case PurchaseStatus.purchased:
case PurchaseStatus.restored:
final valid = await _verifyOnServer(p); // null = unreachable
if (valid == null) {
finish = false; // redelivered next launch
} else if (valid) {
await _deliver(p);
}
case PurchaseStatus.error:
// show p.error?.message
break;
case PurchaseStatus.canceled:
// no error UI
break;
}
if (finish && p.pendingCompletePurchase) {
await _iap.completePurchase(p);
}
}
}
Future<bool?> _verifyOnServer(PurchaseDetails p) async {
// POST p.verificationData.serverVerificationData,
// p.productID and platform to your backend
return true;
}
Future<void> _deliver(PurchaseDetails p) async {/* credit coins / set flag */}
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
PurchaseService.instance.start();
runApp(const MyApp());
}
_verifyOnServer é um placeholder para a chamada ao seu backend. Ele retorna null quando o servidor não pode ser alcançado, de modo que a compra fica incompleta e volta na próxima inicialização. Isso só é seguro se for resolvido dentro da janela de três dias do Android.
| Status | O que o app faz | Chamar completePurchase? |
|---|---|---|
pending | Exibe um estado de espera (por exemplo, Pedir para Comprar/Ask to Buy ou um método de pagamento com processamento demorado) | Não, aguarde o status final |
purchased | Verifica no servidor e depois entrega | Sim, se pendingCompletePurchase |
restored | Verifica e depois concede novamente o direito de acesso | Sim, se pendingCompletePurchase |
error | Exibe error.message | Sim, se pendingCompletePurchase |
canceled | Retorna discretamente ao paywall | Sim, se pendingCompletePurchase |
O que dá errado com compras no aplicativo
Quatro erros comuns fazem você perder compras ou ter o app rejeitado: falta de um botão de restauração, uma inscrição no stream que começa tarde demais, completePurchase nunca ser chamado ou confiar no cliente.
Falta do botão Restaurar. A Diretriz 3.1.1 da Apple determina que os apps ofereçam uma forma de restaurar qualquer compra no aplicativo restaurável. As compras restauradas chegam no purchaseStream com PurchaseStatus.restored, então o botão só precisa chamar restorePurchases(). O handler que você já tem concede o acesso. Coloque-o no paywall e nas configurações:
TextButton(
onPressed: PurchaseService.instance.restore,
child: const Text('Restore Purchases'),
)
Se uma notificação push ou um link de marketing abrir o paywall, faça o roteamento com Deep Linking in Flutter para que o botão Restaurar esteja sempre a um toque de distância.
Começar a escutar tarde demais. Uma compra concluída enquanto o app está fechado, como uma solicitação de Pedir para Comprar aprovada ou um pagamento demorado, é entregue no stream na próxima vez que o app for iniciado. Se o listener for anexado em uma tela que o usuário nunca acessa, essa atualização não é tratada. O usuário não recebe aquilo pelo que pagou e, no Android, uma compra que nunca é concluída é reembolsada após três dias.
Nunca concluir. Se o pendingCompletePurchase de uma compra for true e completePurchase nunca for chamado, o Google Play a reembolsa após três dias e a App Store deixa a transação não finalizada.
Confiar no cliente. Não libere conteúdo com base no status que o seu app recebe. Envie verificationData.serverVerificationData para o seu backend e valide-o lá: com a App Store Server API nas plataformas da Apple ou com a Google Play Developer API no Android. No StoreKit 2, serverVerificationData é uma transação JWS assinada. O seu servidor deve verificar a assinatura, consultar a transação na App Store Server API ou fazer as duas coisas. A requisição de verificação é uma chamada comum ao backend. Se o seu backend usa GraphQL, Consuming GraphQL APIs with Flutter mostra como fazer essa integração.
O design do paywall e SDKs de assinaturas gerenciadas, como o RevenueCat, são escolhas separadas que ficam acima desse fluxo.
Como testar compras sem dinheiro de verdade?
A Apple e o Google oferecem, cada um, um ambiente de teste para compras no aplicativo. A Apple oferece contas sandbox e um arquivo de configuração StoreKit no Xcode. O Google oferece testadores de licença em uma faixa de teste do Play.
- Testadores sandbox da Apple: crie contas sandbox no App Store Connect e faça login com uma delas em um dispositivo. As compras em sandbox usam seus produtos reais do App Store Connect sem cobrar ninguém.
- Arquivo de configuração StoreKit: com um arquivo de configuração StoreKit no Xcode, você pode definir produtos localmente e testar compras no Simulador do iOS antes de concluir a configuração do App Store Connect.
- Testadores de licença do Play: no Play Console, abra Configurações > Teste de licença (Settings > License testing) e adicione contas Google. As compras de teste feitas por testadores de licença não são cobradas em suas contas. O guia de testes de cobrança do Google alerta que testadores de uma faixa de teste que não sejam testadores de licença pagam com dinheiro de verdade.
Teste uma instalação limpa e a restauração nas duas plataformas antes de enviar o app.
Hora de publicar
Para ter um fluxo correto de compras no aplicativo em Flutter: configure as duas lojas, inscreva-se no purchaseStream antes de qualquer outra coisa ser executada, verifique cada compra no seu servidor e conclua toda compra que tenha pendingCompletePurchase definido. Em seguida, crie um produto em cada console, teste-o com uma conta sandbox e um testador de licença e confirme que ele aparece em productDetails, e não em notFoundIDs.
Perguntas frequentes
Como fazer upgrade ou downgrade de uma assinatura no Android com o in_app_purchase?
Chame buyNonConsumable com um GooglePlayPurchaseParam que inclua um ChangeSubscriptionParam. Passe para ele o GooglePlayPurchaseDetails antigo do usuário e, se quiser, um ReplacementMode como ReplacementMode.withTimeProration. Essas classes vêm do in_app_purchase_android, então importe esse pacote quando precisar delas. No iOS, você pode pular esta etapa. Assinaturas do mesmo grupo de assinaturas já substituem umas às outras, então a App Store cuida da troca.
O plugin in_app_purchase funciona no Flutter web?
Não. O plugin precisa de uma loja por trás e só suporta a App Store (no iOS e no macOS) e o Google Play (no Android). Um build Flutter web não tem nenhuma loja por trás, então as vendas na web exigem um processador de pagamentos web, como o Stripe. A restrição das plataformas ao Stripe se aplica a bens digitais vendidos dentro de apps iOS e Android, e não a compras feitas na web.
Qual é a diferença entre o in_app_purchase e o purchases_flutter da RevenueCat?
O in_app_purchase cuida apenas da conexão com a loja. A verificação no servidor, o armazenamento de direitos de acesso e o acompanhamento do estado das assinaturas ficam a cargo do seu próprio backend. O purchases_flutter da RevenueCat encapsula o StoreKit e o Google Play Billing junto com o backend hospedado da RevenueCat, que adiciona validação de recibos no servidor, acompanhamento do status das assinaturas e webhooks. Os dois pacotes continuam passando pelos sistemas de cobrança da Apple e do Google, então as lojas cobram a mesma comissão em qualquer caso.
O que acontece com os assinantes atuais quando eu aumento o preço de uma assinatura?
Depende da loja, da região e do tamanho do aumento. Alguns aumentos exigem a concordância do assinante e, se ele não concordar, a assinatura deixa de ser renovada. As duas lojas também permitem alguns aumentos apenas com aviso prévio. No iOS, o StoreKit exibe por padrão uma tela de consentimento de preço dentro do seu app, e o README do in_app_purchase explica como adiá-la para um momento mais adequado. No Android, o Google Play informa os assinantes sobre a mudança por e-mail e notificação, então você não precisa de código Dart para acioná-la.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k