1. Introdução
Para adicionar compras no app a um app Flutter, é necessário configurar corretamente as lojas do app e da Play, verificar a compra e conceder as permissões necessárias, como vantagens de assinatura.
Neste codelab, você vai adicionar três tipos de compras no app (fornecidos) e verificar essas compras usando um back-end do Dart com o Firebase. O app fornecido, Dash Clicker, contém um jogo que usa o mascote Dash como moeda. Você vai adicionar as seguintes opções de compra:
- Uma opção de compra repetível para 2.000 traços de uma vez.
- Uma compra de upgrade única para transformar o Dash antigo em um Dash moderno.
- Uma assinatura que dobra os cliques gerados automaticamente.
A primeira opção de compra oferece ao usuário um benefício direto de 2.000 Dashes. Eles estão disponíveis diretamente para o usuário e podem ser comprados várias vezes. Isso é chamado de consumível, porque é consumido diretamente e pode ser consumido várias vezes.
A segunda opção faz upgrade do Dash para um Dash mais bonito. Você só precisa comprar o recurso uma vez, e ele fica disponível para sempre. Essa compra é chamada de não consumível porque não pode ser consumida pelo app, mas é válida para sempre.
A terceira e última opção de compra é uma assinatura. Enquanto a assinatura estiver ativa, o usuário vai receber os Dashes mais rapidamente, mas, quando ele parar de pagar pela assinatura, os benefícios também serão perdidos.
O serviço de back-end (também fornecido para você) é executado como um app Dart, verifica se as compras foram feitas e as armazena usando o Firestore. O Firestore é usado para facilitar o processo, mas no app de produção, você pode usar qualquer tipo de serviço de back-end.
O que você vai criar
- Você vai estender um app para oferecer suporte a compras e assinaturas de itens consumíveis.
- Você também vai estender um app de back-end Dart para verificar e armazenar os itens comprados.
O que você vai aprender
- Como configurar a App Store e a Play Store com produtos para compra.
- Como se comunicar com as lojas para verificar compras e armazená-las no Firestore.
- Como gerenciar compras no seu app.
O que é necessário
- Android Studio 4.1 ou versão mais recente
- Xcode 12 ou versão mais recente (para desenvolvimento no iOS)
- SDK do Flutter (em inglês)
2. Configurar o ambiente de desenvolvimento
Para iniciar este codelab, faça o download do código e mude o identificador do pacote para iOS e o nome do pacote para Android.
Fazer o download do código
Para clonar o repositório do GitHub (link em inglês) na linha de comando, use o seguinte comando:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
Ou, se você tiver a ferramenta cli do GitHub instalada, use o seguinte comando:
gh repo clone flutter/codelabs flutter-codelabs
O código de exemplo é clonado em um diretório flutter-codelabs
que contém o código de uma coleção de codelabs. O código deste codelab está em flutter-codelabs/in_app_purchases
.
A estrutura de diretórios em flutter-codelabs/in_app_purchases
contém uma série de snapshots de onde você precisa estar no final de cada etapa nomeada. O código inicial está na etapa 0. Para localizar os arquivos correspondentes, basta fazer o seguinte:
cd flutter-codelabs/in_app_purchases/step_00
Se você quiser pular para a próxima etapa ou conferir como algo fica depois de uma etapa, procure no diretório com o nome da etapa de interesse. O código da última etapa está na pasta complete
.
Configurar o projeto inicial
Abra o projeto inicial de step_00
no seu ambiente de desenvolvimento integrado favorito. Usamos o Android Studio para as capturas de tela, mas o Visual Studio Code também é uma ótima opção. Em qualquer um dos editores, verifique se os plug-ins mais recentes do Dart e do Flutter estão instalados.
Os apps que você vai criar precisam se comunicar com a App Store e a Play Store para saber quais produtos estão disponíveis e por qual preço. Cada app é identificado por um ID exclusivo. Na App Store para iOS, isso é chamado de identificador de pacote, e na Play Store para Android, é o ID do aplicativo. Esses identificadores geralmente são criados usando uma notação de nome de domínio reverso. Por exemplo, ao fazer uma compra no app para flutter.dev, você usaria dev.flutter.inapppurchase
. Pense em um identificador para o app. Você vai definir isso nas configurações do projeto.
Primeiro, configure o identificador do pacote para iOS.
Com o projeto aberto no Android Studio, clique com o botão direito do mouse na pasta iOS, clique em Flutter e abra o módulo no app Xcode.
Na estrutura de pastas do Xcode, o projeto do Runner fica na parte de cima, e as metas Flutter, Runner e Products ficam abaixo do projeto do Runner. Clique duas vezes em Runner para editar as configurações do projeto e clique em Assinatura e recursos. Insira o identificador de pacote que você acabou de escolher no campo Equipe para definir sua equipe.
Agora você pode fechar o Xcode e voltar ao Android Studio para concluir a configuração do Android. Para fazer isso, abra o arquivo build.gradle
em android/app,
e mude o applicationId
(na linha 37 na captura de tela abaixo) para o ID do aplicativo, o mesmo que o identificador do pacote do iOS. Os IDs das lojas do iOS e do Android não precisam ser idênticos, mas mantê-los idênticos é menos propenso a erros. Portanto, neste codelab, também vamos usar identificadores idênticos.
3. Instalar o plug-in
Nesta parte do codelab, você vai instalar o plug-in in_app_purchase.
Adicionar dependência no pubspec
Adicione in_app_purchase
ao pubspec adicionando in_app_purchase
às dependências no pubspec:
$ cd app $ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Abra o pubspec.yaml
e confirme se in_app_purchase
está listado como uma entrada em dependencies
e in_app_purchase_platform_interface
em dev_dependencies
.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cloud_firestore: ^5.5.1
cupertino_icons: ^1.0.8
firebase_auth: ^5.3.4
firebase_core: ^3.8.1
google_sign_in: ^6.2.2
http: ^1.2.2
intl: ^0.20.1
provider: ^6.1.2
in_app_purchase: ^3.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
in_app_purchase_platform_interface: ^1.4.0
Clique em pub get para fazer o download do pacote ou execute flutter pub get
na linha de comando.
4. Configurar a App Store
Para configurar e testar compras no app no iOS, você precisa criar um novo app na App Store e criar produtos para compra. Não é necessário publicar nada ou enviar o app para a Apple analisar. Você precisa ter uma conta de desenvolvedor para fazer isso. Se você não tiver um, inscreva-se no programa para desenvolvedores da Apple.
Contratos de apps pagos
Para usar as compras no app, você também precisa ter um contrato ativo para apps pagos no App Store Connect. Acesse https://appstoreconnect.apple.com/ e clique em Acordos, impostos e bancos.
Aqui você vai encontrar contratos para apps sem custo financeiro e pagos. O status dos apps sem custo financeiro precisa ser "Ativo", e o status dos apps pagos precisa ser "Novo". Leia os termos, aceite-os e insira todas as informações necessárias.
Quando tudo estiver configurado corretamente, o status dos apps pagos vai estar ativo. Isso é muito importante porque você não vai conseguir testar as compras no app sem um contrato ativo.
Registrar o ID do app
Crie um novo identificador no portal de desenvolvedores da Apple.
Escolher os IDs dos apps
Selecione um app
Forneça uma descrição e defina o ID do pacote para corresponder ao mesmo valor definido anteriormente no XCode.
Para mais orientações sobre como criar um novo ID do app, consulte a Ajuda para contas de desenvolvedor .
Como criar um app
Crie um novo app no App Store Connect com seu identificador de pacote exclusivo.
Para mais orientações sobre como criar um app e gerenciar contratos, consulte a ajuda do App Store Connect.
Para testar as compras no app, você precisa de um usuário de teste do sandbox. Esse usuário de teste não pode estar conectado ao iTunes. Ele é usado apenas para testar compras no app. Não é possível usar um endereço de e-mail que já esteja sendo usado em uma conta da Apple. Em Usuários e acesso, acesse Testadores em Sandbox para criar uma nova conta de sandbox ou gerenciar os IDs Apple de sandbox existentes.
Agora você pode configurar o usuário do sandbox no iPhone acessando Ajustes > App Store > Conta de sandbox.
Configurar as compras no app
Agora você vai configurar os três itens que podem ser comprados:
dash_consumable_2k
: uma compra consumível que pode ser feita várias vezes, concedendo ao usuário 2.000 Dashes (a moeda do app) por compra.dash_upgrade_3d
: uma compra de "upgrade" não consumível que só pode ser comprada uma vez e oferece ao usuário um Dash esteticamente diferente para clicar.dash_subscription_doubler
: uma assinatura que concede ao usuário o dobro de Dashes por clique durante a vigência da assinatura.
Acesse Compras no app > Gerenciar.
Crie compras no app com os IDs especificados:
- Configure
dash_consumable_2k
como um consumível.
Use dash_consumable_2k
como o ID do produto. O nome de referência é usado apenas no App Store Connect. Basta defini-lo como dash consumable 2k
e adicionar suas localizações para a compra. Chame a compra Spring is in the air
com 2000 dashes fly out
como descrição.
- Configure
dash_upgrade_3d
como um não consumível.
Use dash_upgrade_3d
como o ID do produto. Defina o nome de referência como dash upgrade 3d
e adicione suas localizações para a compra. Chame a compra 3D Dash
com Brings your dash back to the future
como descrição.
- Configure
dash_subscription_doubler
como uma assinatura com renovação automática.
O fluxo de assinaturas é um pouco diferente. Primeiro, você precisa definir o nome de referência e o ID do produto:
Em seguida, crie um grupo de assinaturas. Quando vários planos fazem parte do mesmo grupo, o usuário só pode assinar um deles por vez, mas pode fazer upgrade ou downgrade entre eles com facilidade. Chame esse grupo de subscriptions
.
Em seguida, insira a duração da assinatura e as localizações. Nomeie esta assinatura como Jet Engine
com a descrição Doubles your clicks
. Clique em Salvar.
Depois de clicar no botão Salvar, adicione um preço de assinatura. Escolha o preço que quiser.
As três compras vão aparecer na lista:
5. Configurar a Play Store
Assim como na App Store, você também precisa de uma conta de desenvolvedor para a Play Store. Se você ainda não tem uma, cadastre uma conta.
Criar um app
Crie um app no Google Play Console:
- Abra o Play Console.
- Selecione Todos os apps > Criar app.
- Selecione um idioma padrão e adicione um título para o app. Digite o nome do app como você quer que ele apareça no Google Play. e pode ser mudado depois.
- Especifique que o app é um jogo. Isso pode ser alterado mais tarde.
- Especifique se o app é sem custo financeiro ou pago.
- Adicione um endereço de e-mail para que os usuários da Play Store possam entrar em contato com você sobre o app.
- Preencha as declarações das diretrizes de conteúdo e das leis de exportação dos EUA.
- Selecione Criar app.
Depois que o app for criado, acesse o painel e conclua todas as tarefas na seção Configurar seu app. Aqui, você fornece algumas informações sobre o app, como classificações de conteúdo e capturas de tela.
Assinar o aplicativo
Para testar as compras no app, é necessário ter pelo menos um build enviado para o Google Play.
Para isso, o build de lançamento precisa ser assinado com algo diferente das chaves de depuração.
Criar um keystore
Se você já tiver um keystore, pule para a próxima etapa. Caso contrário, crie um executando o seguinte na linha de comando.
No Mac/Linux, use o seguinte comando:
keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
No Windows, use o seguinte comando:
keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key
Esse comando armazena o arquivo key.jks
no seu diretório inicial. Se você quiser armazenar o arquivo em outro lugar, mude o argumento transmitido para o parâmetro -keystore
. Mantenha o
keystore
arquivo particular; não o registre no controle de origem público.
Fazer referência ao keystore do app
Crie um arquivo chamado <your app dir>/android/key.properties
que contenha uma referência ao keystore:
storePassword=<password from previous step>
keyPassword=<password from previous step>
keyAlias=key
storeFile=<location of the key store file, such as /Users/<user name>/key.jks>
Configurar a assinatura no Gradle
Configure a assinatura do app editando o arquivo <your app dir>/android/app/build.gradle
.
Adicione as informações do keystore do arquivo de propriedades antes do bloco android
:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
// omitted
}
Carregue o arquivo key.properties
no objeto keystoreProperties
.
Adicione o seguinte código antes do bloco buildTypes
:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now,
// so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
Configure o bloco signingConfigs
no arquivo build.gradle
do módulo com as informações de configuração de assinatura:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
Os builds de lançamento do app agora são assinados automaticamente.
Para saber mais sobre como assinar seu app, consulte Assinar o app em developer.android.com.
Enviar seu primeiro build
Depois que o app estiver configurado para assinatura, será possível criar o aplicativo executando:
flutter build appbundle
Esse comando gera um build de lançamento por padrão, e a saída pode ser encontrada em <your app dir>/build/app/outputs/bundle/release/
.
No painel do Google Play Console, acesse Versões > Testes > Teste fechado e crie uma nova versão de teste fechado.
Neste codelab, você vai usar a assinatura do Google para o app. Clique em Continuar em Assinatura de apps do Google Play para ativar a opção.
Em seguida, faça upload do pacote de apps app-release.aab
gerado pelo comando de build.
Clique em Salvar e em Avaliar versão.
Por fim, clique em Iniciar lançamento para testes internos para ativar a versão de teste interno.
Configurar usuários de teste
Para testar as compras no app, as Contas do Google dos testadores precisam ser adicionadas ao Google Play Console em dois locais:
- Para a faixa de teste específica (teste interno)
- Como testador de licença
Primeiro, adicione o testador à faixa de teste interno. Volte para Versão > Testes > Teste interno e clique na guia Testadores.
Clique em Criar lista de e-mails para criar uma nova lista. Dê um nome à lista e adicione os endereços de e-mail das Contas do Google que precisam de acesso para testar as compras no app.
Em seguida, marque a caixa de seleção da lista e clique em Salvar alterações.
Em seguida, adicione os testadores de licença:
- Volte para a visualização Todos os apps do Google Play Console.
- Acesse Configurações > Teste de licença.
- Adicione os mesmos endereços de e-mail dos testadores que precisam testar as compras no app.
- Defina Resposta de licença como
RESPOND_NORMALLY
. - Clique em Salvar alterações.
Configurar as compras no app
Agora você vai configurar os itens que podem ser comprados no app.
Assim como na App Store, você precisa definir três compras diferentes:
dash_consumable_2k
: uma compra consumível que pode ser feita várias vezes, concedendo ao usuário 2.000 Dashes (a moeda do app) por compra.dash_upgrade_3d
: uma compra de "upgrade" não consumível que só pode ser comprada uma vez, o que dá ao usuário um Dash esteticamente diferente para clicar.dash_subscription_doubler
: uma assinatura que concede ao usuário o dobro de Dashes por clique durante a vigência da assinatura.
Primeiro, adicione o consumível e o não consumível.
- Acesse o Google Play Console e selecione seu app.
- Acesse Monetização > Produtos > Produtos no app.
- Clique em Criar produto
.
- Insira todas as informações necessárias para o produto. Verifique se o ID do produto corresponde exatamente ao ID que você pretende usar.
- Clique em Salvar.
- Clique em Ativar.
- Repita o processo para a compra de "upgrade" não consumível.
Em seguida, adicione a assinatura:
- Acesse o Google Play Console e selecione seu app.
- Acesse Monetização > Produtos > Assinaturas.
- Clique em Criar assinatura
.
- Insira todas as informações necessárias para sua assinatura. Verifique se o ID do produto corresponde exatamente ao ID que você pretende usar.
- Clique em Salvar.
Suas compras agora estão configuradas no Play Console.
6. Configurar o Firebase
Neste codelab, você vai usar um serviço de back-end para verificar e rastrear as compras dos usuários.
O uso de um serviço de back-end tem vários benefícios:
- Você pode verificar transações com segurança.
- É possível reagir a eventos de faturamento das app stores.
- Você pode acompanhar as compras em um banco de dados.
- Os usuários não vão conseguir enganar seu app para oferecer recursos premium ao retroceder o relógio do sistema.
Há muitas maneiras de configurar um serviço de back-end, mas você vai fazer isso usando as funções do Cloud e o Firestore, além do Firebase do Google.
A criação do back-end está fora do escopo deste codelab. Por isso, o código inicial já inclui um projeto do Firebase que processa compras básicas para você começar.
Os plug-ins do Firebase também estão incluídos no app inicial.
Agora, você precisa criar seu próprio projeto do Firebase, configurar o app e o back-end para o Firebase e, por fim, implantar o back-end.
Criar um projeto do Firebase
Acesse o console do Firebase e crie um novo projeto. Para este exemplo, chame o projeto de Dash Clicker.
No app de back-end, você vincula as compras a um usuário específico. Portanto, é necessária a autenticação. Para isso, use o módulo de autenticação do Firebase com o Login do Google.
- No painel do Firebase, acesse Autenticação e ative-a, se necessário.
- Acesse a guia Método de login e ative o provedor de login do Google.
Ative também esse recurso, porque você também vai usar o banco de dados do Firestore do Firebase.
Defina regras do Cloud Firestore assim:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /purchases/{purchaseId} {
allow read: if request.auth != null && request.auth.uid == resource.data.userId
}
}
}
Configurar o Firebase para Flutter
A maneira recomendada de instalar o Firebase no app Flutter é usar a CLI do FlutterFire. Siga as instruções explicadas na página de configuração.
Ao executar o flutterfire configure, selecione o projeto que você acabou de criar na etapa anterior.
$ flutterfire configure
i Found 5 Firebase projects.
? Select a Firebase project to configure your Flutter application with ›
❯ in-app-purchases-1234 (in-app-purchases-1234)
other-flutter-codelab-1 (other-flutter-codelab-1)
other-flutter-codelab-2 (other-flutter-codelab-2)
other-flutter-codelab-3 (other-flutter-codelab-3)
other-flutter-codelab-4 (other-flutter-codelab-4)
<create a new project>
Em seguida, ative iOS e Android selecionando as duas plataformas.
? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
macos
web
Quando solicitado a substituir firebase_options.dart, selecione "Sim".
? Generated FirebaseOptions file lib/firebase_options.dart already exists, do you want to override it? (y/n) › yes
Configurar o Firebase para Android: outras etapas
No painel do Firebase, acesse Visão geral do projeto,escolha Configurações e selecione a guia Geral.
Role a tela para baixo até Seus apps e selecione dashclicker (android).
Para permitir o Login do Google no modo de depuração, informe a impressão digital do hash SHA-1 do certificado de depuração.
Acessar o hash do certificado de assinatura de depuração
Na raiz do projeto do app Flutter, mude o diretório para a pasta android/
e gere um relatório de assinatura.
cd android ./gradlew :app:signingReport
Você vai encontrar uma lista grande de chaves de assinatura. Como você está procurando o hash do certificado de depuração, procure o certificado com as propriedades Variant
e Config
definidas como debug
. É provável que o keystore esteja na pasta inicial em .android/debug.keystore
.
> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038
Copie o hash SHA-1 e preencha o último campo na caixa de diálogo modal de envio do app.
Configurar o Firebase para iOS: outras etapas
Abra o ios/Runnder.xcworkspace
com Xcode
. Ou com o ambiente de desenvolvimento integrado que você preferir.
No VSCode, clique com o botão direito do mouse na pasta ios/
e depois em open in xcode
.
No Android Studio, clique com o botão direito do mouse na pasta ios/
e clique em flutter
seguido da opção open iOS module in Xcode
.
Para permitir o login do Google no iOS, adicione a opção de configuração CFBundleURLTypes
aos arquivos plist
do build. Consulte os documentos do pacote google_sign_in
para mais informações. Nesse caso, os arquivos são ios/Runner/Info-Debug.plist
e ios/Runner/Info-Release.plist
.
O par de chave-valor já foi adicionado, mas os valores precisam ser substituídos:
- Receba o valor de
REVERSED_CLIENT_ID
do arquivoGoogleService-Info.plist
, sem o elemento<string>..</string>
ao redor. - Substitua o valor nos arquivos
ios/Runner/Info-Debug.plist
eios/Runner/Info-Release.plist
na chaveCFBundleURLTypes
.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- TODO Replace this value: -->
<!-- Copied from GoogleService-Info.plist key REVERSED_CLIENT_ID -->
<string>com.googleusercontent.apps.REDACTED</string>
</array>
</dict>
</array>
A configuração do Firebase foi concluída.
7. Ouvir atualizações de compra
Nesta parte do codelab, você vai preparar o app para comprar os produtos. Esse processo inclui a detecção de atualizações de compra e erros após a inicialização do app.
Ouvir atualizações de compras
Em main.dart,
, encontre o widget MyHomePage
que tem uma Scaffold
com uma BottomNavigationBar
contendo duas páginas. Essa página também cria três Provider
s para DashCounter
, DashUpgrades,
e DashPurchases
. DashCounter
rastreia a contagem atual de traços e os incrementa automaticamente. O DashUpgrades
gerencia os upgrades que podem ser comprados com as barras. Este codelab se concentra em DashPurchases
.
Por padrão, o objeto de um provedor é definido quando ele é solicitado pela primeira vez. Esse objeto ouve as atualizações de compra diretamente quando o app é iniciado. Portanto, desative o carregamento lento nesse objeto com lazy: false
:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
),
lazy: false, // Add this line
),
Você também precisa de uma instância do InAppPurchaseConnection
. No entanto, para manter o app testável, você precisa de uma maneira de simular a conexão. Para fazer isso, crie um método de instância que possa ser substituído no teste e adicione-o a main.dart
.
lib/main.dart
// Gives the option to override in tests.
class IAPConnection {
static InAppPurchase? _instance;
static set instance(InAppPurchase value) {
_instance = value;
}
static InAppPurchase get instance {
_instance ??= InAppPurchase.instance;
return _instance!;
}
}
É necessário atualizar um pouco o teste se você quiser que ele continue funcionando.
test/widget_test.dart
import 'package:dashclicker/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; // And this import
void main() {
testWidgets('App starts', (tester) async {
IAPConnection.instance = TestIAPConnection(); // Add this line
await tester.pumpWidget(const MyApp());
expect(find.text('Tim Sneath'), findsOneWidget);
});
}
class TestIAPConnection implements InAppPurchase { // Add from here
@override
Future<bool> buyConsumable(
{required PurchaseParam purchaseParam, bool autoConsume = true}) {
return Future.value(false);
}
@override
Future<bool> buyNonConsumable({required PurchaseParam purchaseParam}) {
return Future.value(false);
}
@override
Future<void> completePurchase(PurchaseDetails purchase) {
return Future.value();
}
@override
Future<bool> isAvailable() {
return Future.value(false);
}
@override
Future<ProductDetailsResponse> queryProductDetails(Set<String> identifiers) {
return Future.value(ProductDetailsResponse(
productDetails: [],
notFoundIDs: [],
));
}
@override
T getPlatformAddition<T extends InAppPurchasePlatformAddition?>() {
// TODO: implement getPlatformAddition
throw UnimplementedError();
}
@override
Stream<List<PurchaseDetails>> get purchaseStream =>
Stream.value(<PurchaseDetails>[]);
@override
Future<void> restorePurchases({String? applicationUserName}) {
// TODO: implement restorePurchases
throw UnimplementedError();
}
@override
Future<String> countryCode() {
// TODO: implement countryCode
throw UnimplementedError();
}
} // To here.
Em lib/logic/dash_purchases.dart
, acesse o código de DashPurchases ChangeNotifier
. No momento, só é possível adicionar um DashCounter
aos Dashes comprados.
Adicione uma propriedade de assinatura de fluxo, _subscription
(do tipo StreamSubscription<List<PurchaseDetails>> _subscription;
), o IAPConnection.instance,
e as importações. O código resultante vai ficar assim:
lib/logic/dash_purchases.dart
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart'; // Add this import
import '../main.dart'; // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.available;
late StreamSubscription<List<PurchaseDetails>> _subscription; // Add this line
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance; // And this line
DashPurchases(this.counter);
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
}
A palavra-chave late
é adicionada a _subscription
porque a _subscription
é inicializada no construtor. Esse projeto está configurado para não permitir valores nulos por padrão (NNBD, na sigla em inglês), o que significa que as propriedades que não são declaradas como anuláveis precisam ter um valor não nulo. O qualificador late
permite atrasar a definição desse valor.
No construtor, acesse o stream purchaseUpdated
e comece a detectar o stream. No método dispose()
, cancele a assinatura do stream.
lib/logic/dash_purchases.dart
class DashPurchases extends ChangeNotifier {
DashCounter counter;
StoreState storeState = StoreState.notAvailable; // Modify this line
late StreamSubscription<List<PurchaseDetails>> _subscription;
List<PurchasableProduct> products = [
PurchasableProduct(
'Spring is in the air',
'Many dashes flying out from their nests',
'\$0.99',
),
PurchasableProduct(
'Jet engine',
'Doubles you clicks per second for a day',
'\$1.99',
),
];
bool get beautifiedDash => false;
final iapConnection = IAPConnection.instance;
DashPurchases(this.counter) { // Add from here
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
} // To here.
Future<void> buy(PurchasableProduct product) async {
product.status = ProductStatus.pending;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchased;
notifyListeners();
await Future<void>.delayed(const Duration(seconds: 5));
product.status = ProductStatus.purchasable;
notifyListeners();
}
// Add from here
void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
// Handle purchases here
}
void _updateStreamOnDone() {
_subscription.cancel();
}
void _updateStreamOnError(dynamic error) {
//Handle error here
} // To here.
}
Agora, o app recebe as atualizações de compra. Na próxima seção, você vai fazer uma compra.
Antes de continuar, execute os testes com "flutter test"
para verificar se tudo está configurado corretamente.
$ flutter test
00:01 +1: All tests passed!
8. Fazer compras
Nesta parte do codelab, você vai substituir os produtos fictícios atuais por produtos reais que podem ser comprados. Esses produtos são carregados das lojas, mostrados em uma lista e comprados quando você toca neles.
Adapt PurchasableProduct
PurchasableProduct
mostra um produto simulado. Atualize-o para mostrar o conteúdo real substituindo a classe PurchasableProduct
em purchasable_product.dart
pelo seguinte código:
lib/model/purchasable_product.dart
import 'package:in_app_purchase/in_app_purchase.dart';
enum ProductStatus {
purchasable,
purchased,
pending,
}
class PurchasableProduct {
String get id => productDetails.id;
String get title => productDetails.title;
String get description => productDetails.description;
String get price => productDetails.price;
ProductStatus status;
ProductDetails productDetails;
PurchasableProduct(this.productDetails) : status = ProductStatus.purchasable;
}
Em dash_purchases.dart,
, remova as compras fictícias e substitua por uma lista vazia, List<PurchasableProduct> products = [];
Carregar as compras disponíveis
Para permitir que um usuário faça uma compra, carregue as compras da loja. Primeiro, verifique se a loja está disponível. Quando a loja não está disponível, a configuração storeState
como notAvailable
mostra uma mensagem de erro para o usuário.
lib/logic/dash_purchases.dart
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
}
Quando a loja estiver disponível, carregue as compras disponíveis. Considerando a configuração anterior do Firebase, storeKeyConsumable
, storeKeySubscription,
e storeKeyUpgrade
devem aparecer. Quando uma compra esperada não estiver disponível, mostre essas informações no console. Você também pode enviar essas informações para o serviço de back-end.
O método await iapConnection.queryProductDetails(ids)
retorna os IDs que não foram encontrados e os produtos que podem ser comprados. Use o productDetails
da resposta para atualizar a interface e defina o StoreState
como available
.
lib/logic/dash_purchases.dart
import '../constants.dart';
Future<void> loadPurchases() async {
final available = await iapConnection.isAvailable();
if (!available) {
storeState = StoreState.notAvailable;
notifyListeners();
return;
}
const ids = <String>{
storeKeyConsumable,
storeKeySubscription,
storeKeyUpgrade,
};
final response = await iapConnection.queryProductDetails(ids);
products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
storeState = StoreState.available;
notifyListeners();
}
Chame a função loadPurchases()
no construtor:
lib/logic/dash_purchases.dart
DashPurchases(this.counter) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
loadPurchases();
}
Por fim, mude o valor do campo storeState
de StoreState.available
para StoreState.loading:
.
lib/logic/dash_purchases.dart
StoreState storeState = StoreState.loading;
Mostrar os produtos que podem ser comprados
Considere o arquivo purchase_page.dart
. O widget PurchasePage
mostra _PurchasesLoading
, _PurchaseList,
ou _PurchasesNotAvailable,
, dependendo do StoreState
. O widget também mostra as compras anteriores do usuário, que são usadas na próxima etapa.
O widget _PurchaseList
mostra a lista de produtos que podem ser comprados e envia uma solicitação de compra ao objeto DashPurchases
.
lib/pages/purchase_page.dart
class _PurchaseList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var purchases = context.watch<DashPurchases>();
var products = purchases.products;
return Column(
children: products
.map((product) => _PurchaseWidget(
product: product,
onPressed: () {
purchases.buy(product);
}))
.toList(),
);
}
}
Os produtos disponíveis nas lojas do Android e do iOS vão aparecer se estiverem configurados corretamente. Pode levar algum tempo até que as compras fiquem disponíveis quando inseridas nos respectivos consoles.
Volte para dash_purchases.dart
e implemente a função para comprar um produto. Você só precisa separar os itens consumíveis dos não consumíveis. O upgrade e os produtos de assinatura não são consumíveis.
lib/logic/dash_purchases.dart
Future<void> buy(PurchasableProduct product) async {
final purchaseParam = PurchaseParam(productDetails: product.productDetails);
switch (product.id) {
case storeKeyConsumable:
await iapConnection.buyConsumable(purchaseParam: purchaseParam);
case storeKeySubscription:
case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
default:
throw ArgumentError.value(
product.productDetails, '${product.id} is not a known product');
}
}
Antes de continuar, crie a variável _beautifiedDashUpgrade
e atualize o getter beautifiedDash
para fazer referência a ela.
lib/logic/dash_purchases.dart
bool get beautifiedDash => _beautifiedDashUpgrade;
bool _beautifiedDashUpgrade = false;
O método _onPurchaseUpdate
recebe as atualizações de compra, atualiza o status do produto mostrado na página de compra e aplica a compra à lógica do contador. É importante chamar completePurchase
depois de processar a compra para que a loja saiba que ela foi processada corretamente.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(2000);
case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
9. configurar o back-end
Antes de passar para o rastreamento e a verificação de compras, configure um back-end do Dart para isso.
Nesta seção, trabalhe com a pasta dart-backend/
como raiz.
Verifique se você tem as seguintes ferramentas instaladas:
- Dart
- Firebase CLI
Visão geral do projeto base
Como algumas partes deste projeto são consideradas fora do escopo deste codelab, elas estão incluídas no código inicial. É recomendável analisar o que já está no código inicial antes de começar, para ter uma ideia de como você vai estruturar as coisas.
Esse código de back-end pode ser executado localmente na sua máquina. Não é necessário implantá-lo para usá-lo. No entanto, é necessário se conectar do seu dispositivo de desenvolvimento (Android ou iPhone) à máquina em que o servidor será executado. Para isso, eles precisam estar na mesma rede, e você precisa saber o endereço IP da sua máquina.
Tente executar o servidor usando o seguinte comando:
$ dart ./bin/server.dart
Serving at http://0.0.0.0:8080
O back-end do Dart usa shelf
e shelf_router
para exibir endpoints de API. Por padrão, o servidor não fornece rotas. Mais tarde, você vai criar uma rota para processar a verificação de compra.
Uma parte que já está incluída no código inicial é a IapRepository
em lib/iap_repository.dart
. Como aprender a interagir com o Firestore ou bancos de dados em geral não é relevante para este codelab, o código inicial contém funções para criar ou atualizar compras no Firestore, bem como todas as classes dessas compras.
Configurar o acesso ao Firebase
Para acessar o Firebase Firestore, você precisa de uma chave de acesso à conta de serviço. Gere uma ao abrir as configurações do projeto do Firebase e navegar até a seção Contas de serviço. Em seguida, selecione Gerar nova chave privada.
Copie o arquivo JSON salvo para a pasta assets/
e renomeie-o como service-account-firebase.json
.
Configurar o acesso ao Google Play
Para acessar a Play Store e verificar as compras, você precisa gerar uma conta de serviço com essas permissões e fazer o download das credenciais JSON.
- Acesse o Google Play Console e comece pela página Todos os apps.
- Acesse Configuração > Acesso à API.
Caso o Google Play Console solicite que você crie ou vincule a um projeto existente, faça isso primeiro e depois volte a esta página.
- Encontre a seção em que você pode definir contas de serviço e clique em Criar nova conta de serviço.
- Clique no link Google Cloud Platform na caixa de diálogo que aparece.
- Selecione o projeto. Se não encontrar, verifique se você fez login na Conta do Google correta na lista suspensa Conta no canto superior direito.
- Depois de selecionar o projeto, clique em + Criar conta de serviço na barra de menu superior.
- Dê um nome à conta de serviço e, se quiser, uma descrição para não esquecer para que ela serve. Em seguida, avance para a próxima etapa.
- Atribua o papel de editor à conta de serviço.
- Conclua o assistente, volte para a página Acesso à API no console do desenvolvedor e clique em Atualizar contas de serviço. A conta recém-criada vai aparecer na lista.
- Clique em Conceder acesso para sua nova conta de serviço.
- Role para baixo na próxima página até o bloco Dados financeiros. Selecione Ver dados financeiros, pedidos e respostas à pesquisa de cancelamento e Gerenciar pedidos e assinaturas.
- Clique em Convidar usuário.
- Agora que a conta está configurada, você só precisa gerar algumas credenciais. No console do Cloud, encontre sua conta de serviço na lista de contas de serviço, clique nos três pontos verticais e escolha Gerenciar chaves.
- Crie e faça o download de uma nova chave JSON.
- Renomeie o arquivo transferido por download como
service-account-google-play.json,
e mova-o para o diretórioassets/
.
Mais uma coisa que precisamos fazer é abrir lib/constants.dart,
e substituir o valor de androidPackageId
pelo ID do pacote escolhido para o app Android.
Configurar o acesso à Apple App Store
Para acessar a App Store e verificar as compras, você precisa configurar uma senha secreta compartilhada:
- Abra o App Store Connect.
- Acesse Meus apps e selecione o app.
- No painel de navegação, acesse Compras no app > Gerenciar.
- No canto superior direito da lista, clique em Secreto compartilhado específico do app.
- Gere um novo secret e copie-o.
- Abra
lib/constants.dart,
e substitua o valor deappStoreSharedSecret
pela chave secreta compartilhada que você acabou de gerar.
Arquivo de configuração de constantes
Antes de continuar, verifique se as seguintes constantes estão configuradas no arquivo lib/constants.dart
:
androidPackageId
: ID do pacote usado no Android. Por exemplo,com.example.dashclicker
appStoreSharedSecret
: chave secreta compartilhada para acessar o App Store Connect e realizar a verificação de compra.bundleId
: ID do pacote usado no iOS. Por exemplo,com.example.dashclicker
Por enquanto, você pode ignorar o restante das constantes.
10. Verificar compras
O fluxo geral para verificar compras é semelhante no iOS e no Android.
Para as duas lojas, seu app recebe um token quando uma compra é feita.
Esse token é enviado pelo app para o serviço de back-end, que, por sua vez, verifica a compra com os servidores da respectiva loja usando o token fornecido.
O serviço de back-end pode armazenar a compra e responder ao aplicativo se ela foi válida ou não.
Ao fazer com que o serviço de back-end faça a validação com as lojas em vez do aplicativo em execução no dispositivo do usuário, é possível impedir que o usuário tenha acesso a recursos premium, por exemplo, rebobinando o relógio do sistema.
Configurar o lado do Flutter
Configurar a autenticação
Como você vai enviar as compras para seu serviço de back-end, é importante garantir que o usuário seja autenticado ao fazer uma compra. A maior parte da lógica de autenticação já foi adicionada ao projeto inicial. Basta garantir que o PurchasePage
mostre o botão de login quando o usuário ainda não tiver feito login. Adicione o seguinte código ao início do método de build de PurchasePage
:
lib/pages/purchase_page.dart
import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';
class PurchasePage extends StatelessWidget {
const PurchasePage({super.key});
@override
Widget build(BuildContext context) {
var firebaseNotifier = context.watch<FirebaseNotifier>();
if (firebaseNotifier.state == FirebaseState.loading) {
return _PurchasesLoading();
} else if (firebaseNotifier.state == FirebaseState.notAvailable) {
return _PurchasesNotAvailable();
}
if (!firebaseNotifier.loggedIn) {
return const LoginPage();
}
// omitted
Chamar o endpoint de verificação do app
No app, crie a função _verifyPurchase(PurchaseDetails purchaseDetails)
que chama o endpoint /verifypurchase
no back-end do Dart usando uma chamada HTTP POST.
Envie a loja selecionada (google_play
para a Play Store ou app_store
para a App Store), o serverVerificationData
e o productID
. O servidor retorna o código de status indicando se a compra foi verificada.
Nas constantes do app, configure o IP do servidor para o endereço IP da máquina local.
lib/logic/dash_purchases.dart
FirebaseNotifier firebaseNotifier;
DashPurchases(this.counter, this.firebaseNotifier) {
// omitted
}
Adicione o firebaseNotifier
com a criação de DashPurchases
em main.dart:
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
),
lazy: false,
),
Adicione um getter para o usuário no FirebaseNotifier para transmitir o ID do usuário à função de verificação de compra.
lib/logic/firebase_notifier.dart
User? get user => FirebaseAuth.instance.currentUser;
Adicione a função _verifyPurchase
à classe DashPurchases
. Essa função async
retorna um booleano indicando se a compra foi validada.
lib/logic/dash_purchases.dart
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
final url = Uri.parse('http://$serverIp:8080/verifypurchase');
const headers = {
'Content-type': 'application/json',
'Accept': 'application/json',
};
final response = await http.post(
url,
body: jsonEncode({
'source': purchaseDetails.verificationData.source,
'productId': purchaseDetails.productID,
'verificationData':
purchaseDetails.verificationData.serverVerificationData,
'userId': firebaseNotifier.user?.uid,
}),
headers: headers,
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
}
Chame a função _verifyPurchase
em _handlePurchase
antes de aplicar a compra. Só aplique a compra quando ela for verificada. Em um app de produção, é possível especificar isso para, por exemplo, aplicar uma assinatura de teste quando a loja estiver temporariamente indisponível. No entanto, para este exemplo, mantenha a simplicidade e só aplique a compra quando ela for verificada.
lib/logic/dash_purchases.dart
Future<void> _onPurchaseUpdate(
List<PurchaseDetails> purchaseDetailsList) async {
for (var purchaseDetails in purchaseDetailsList) {
await _handlePurchase(purchaseDetails);
}
notifyListeners();
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.purchased) {
// Send to server
var validPurchase = await _verifyPurchase(purchaseDetails);
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case storeKeySubscription:
counter.applyPaidMultiplier();
case storeKeyConsumable:
counter.addBoughtDashes(1000);
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await iapConnection.completePurchase(purchaseDetails);
}
}
No app, tudo está pronto para validar as compras.
Configurar o serviço de back-end
Em seguida, configure a função do Cloud para verificar compras no back-end.
Criar manipuladores de compra
Como o fluxo de verificação das duas lojas é quase idêntico, configure uma classe PurchaseHandler
abstrata com implementações separadas para cada loja.
Comece adicionando um arquivo purchase_handler.dart
à pasta lib/
, em que você define uma classe PurchaseHandler
abstrata com dois métodos abstratos para verificar dois tipos diferentes de compras: assinaturas e não assinaturas.
lib/purchase_handler.dart
import 'products.dart';
/// Generic purchase handler,
/// must be implemented for Google Play and Apple Store
abstract class PurchaseHandler {
/// Verify if non-subscription purchase (aka consumable) is valid
/// and update the database
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
});
/// Verify if subscription purchase (aka non-consumable) is valid
/// and update the database
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
});
}
Como você pode ver, cada método requer três parâmetros:
userId:
O ID do usuário conectado para que você possa vincular as compras a ele.productData:
Dados sobre o produto. Você vai definir isso em um minuto.token:
O token fornecido à loja pelo usuário.
Além disso, para facilitar o uso desses processadores de compra, adicione um método verifyPurchase()
que possa ser usado para assinaturas e não assinaturas:
lib/purchase_handler.dart
/// Verify if purchase is valid and update the database
Future<bool> verifyPurchase({
required String userId,
required ProductData productData,
required String token,
}) async {
switch (productData.type) {
case ProductType.subscription:
return handleSubscription(
userId: userId,
productData: productData,
token: token,
);
case ProductType.nonSubscription:
return handleNonSubscription(
userId: userId,
productData: productData,
token: token,
);
}
}
Agora, você pode chamar verifyPurchase
para ambos os casos, mas ainda ter implementações separadas.
A classe ProductData
contém informações básicas sobre os diferentes produtos que podem ser comprados, incluindo o ID do produto (às vezes também chamado de SKU) e o ProductType
.
lib/products.dart
class ProductData {
final String productId;
final ProductType type;
const ProductData(this.productId, this.type);
}
O ProductType
pode ser uma assinatura ou não.
lib/products.dart
enum ProductType {
subscription,
nonSubscription,
}
Por fim, a lista de produtos é definida como um mapa no mesmo arquivo.
lib/products.dart
const productDataMap = {
'dash_consumable_2k': ProductData(
'dash_consumable_2k',
ProductType.nonSubscription,
),
'dash_upgrade_3d': ProductData(
'dash_upgrade_3d',
ProductType.nonSubscription,
),
'dash_subscription_doubler': ProductData(
'dash_subscription_doubler',
ProductType.subscription,
),
};
Em seguida, defina algumas implementações de marcador de posição para a Google Play Store e a App Store da Apple. Comece com o Google Play:
Crie lib/google_play_purchase_handler.dart
e adicione uma classe que estenda o PurchaseHandler
que você acabou de escrever:
lib/google_play_purchase_handler.dart
import 'dart:async';
import 'package:googleapis/androidpublisher/v3.dart' as ap;
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
return true;
}
}
Por enquanto, ele retorna true
para os métodos de gerenciador. Você vai conhecer esses métodos mais tarde.
Como você pode ter notado, o construtor recebe uma instância do IapRepository
. O gerenciador de compras usa essa instância para armazenar informações sobre compras no Firestore mais tarde. Para se comunicar com o Google Play, use o AndroidPublisherApi
fornecido.
Em seguida, faça o mesmo para o gerenciador da app store. Crie lib/app_store_purchase_handler.dart
e adicione uma classe que estenda o PurchaseHandler
novamente:
lib/app_store_purchase_handler.dart
import 'dart:async';
import 'package:app_store_server_sdk/app_store_server_sdk.dart';
import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';
class AppStorePurchaseHandler extends PurchaseHandler {
final IapRepository iapRepository;
AppStorePurchaseHandler(
this.iapRepository,
);
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return true;
}
}
Ótimo! Agora você tem dois processadores de compra. Em seguida, vamos criar o endpoint da API de verificação de compra.
Usar processadores de compra
Abra bin/server.dart
e crie um endpoint da API usando shelf_route
:
bin/server.dart
Future<void> main() async {
final router = Router();
final purchaseHandlers = await _createPurchaseHandlers();
router.post('/verifypurchase', (Request request) async {
final dynamic payload = json.decode(await request.readAsString());
final (:userId, :source, :productData, :token) = getPurchaseData(payload);
final result = await purchaseHandlers[source]!.verifyPurchase(
userId: userId,
productData: productData,
token: token,
);
if (result) {
return Response.ok('all good!');
} else {
return Response.internalServerError();
}
});
await serveHandler(router.call);
}
({
String userId,
String source,
ProductData productData,
String token,
}) getPurchaseData(dynamic payload) {
if (payload
case {
'userId': String userId,
'source': String source,
'productId': String productId,
'verificationData': String token,
}) {
return (
userId: userId,
source: source,
productData: productDataMap[productId]!,
token: token,
);
} else {
throw const FormatException('Unexpected JSON');
}
}
O código acima está fazendo o seguinte:
- Defina um endpoint POST que será chamado pelo app criado anteriormente.
- Decodificar o payload JSON e extrair as seguintes informações:
userId
: ID do usuário conectado no momentosource
: loja usada,app_store
ougoogle_play
.productData
: obtido doproductDataMap
que você criou anteriormente.token
: contém os dados de verificação a serem enviados às lojas.- Chame o método
verifyPurchase
para oGooglePlayPurchaseHandler
ou oAppStorePurchaseHandler
, dependendo da origem. - Se a verificação for bem-sucedida, o método vai retornar um
Response.ok
para o cliente. - Se a verificação falhar, o método vai retornar um
Response.internalServerError
ao cliente.
Depois de criar o endpoint da API, você precisa configurar os dois processadores de compra. Para isso, é necessário carregar as chaves da conta de serviço que você recebeu na etapa anterior e configurar o acesso aos diferentes serviços, incluindo a API Android Publisher e a API Firestore do Firebase. Em seguida, crie os dois processadores de compra com as diferentes dependências:
bin/server.dart
Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
// Configure Android Publisher API access
final serviceAccountGooglePlay =
File('assets/service-account-google-play.json').readAsStringSync();
final clientCredentialsGooglePlay =
auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
]);
final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);
// Configure Firestore API access
final serviceAccountFirebase =
File('assets/service-account-firebase.json').readAsStringSync();
final clientCredentialsFirebase =
auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
final clientFirebase =
await auth.clientViaServiceAccount(clientCredentialsFirebase, [
fs.FirestoreApi.cloudPlatformScope,
]);
final firestoreApi = fs.FirestoreApi(clientFirebase);
final dynamic json = jsonDecode(serviceAccountFirebase);
final projectId = json['project_id'] as String;
final iapRepository = IapRepository(firestoreApi, projectId);
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
}
Verificar compras no Android: implementar o manipulador de compras
Em seguida, continue a implementação do gerenciador de compras do Google Play.
O Google já oferece pacotes Dart para interagir com as APIs necessárias para verificar compras. Você as inicializou no arquivo server.dart
e agora as usa na classe GooglePlayPurchaseHandler
.
Implemente o gerenciador para compras que não são do tipo assinatura:
lib/google_play_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleNonSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.products.get(
androidPackageId,
productData.productId,
token,
);
print('Purchases response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = response.orderId!;
final purchaseData = NonSubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.purchaseTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _nonSubscriptionStatusFrom(response.purchaseState),
userId: userId,
iapSource: IAPSource.googleplay,
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle NonSubscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle NonSubscription: $e\n');
}
return false;
}
É possível atualizar o gerenciador de compra de assinatura de maneira semelhante:
lib/google_play_purchase_handler.dart
/// Handle subscription purchases.
///
/// Retrieves the purchase status from Google Play and updates
/// the Firestore Database accordingly.
@override
Future<bool> handleSubscription({
required String? userId,
required ProductData productData,
required String token,
}) async {
print(
'GooglePlayPurchaseHandler.handleSubscription'
'($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
);
try {
// Verify purchase with Google
final response = await androidPublisher.purchases.subscriptions.get(
androidPackageId,
productData.productId,
token,
);
print('Subscription response: ${response.toJson()}');
// Make sure an order id exists
if (response.orderId == null) {
print('Could not handle purchase without order id');
return false;
}
final orderId = extractOrderId(response.orderId!);
final purchaseData = SubscriptionPurchase(
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.startTimeMillis ?? '0'),
),
orderId: orderId,
productId: productData.productId,
status: _subscriptionStatusFrom(response.paymentState),
userId: userId,
iapSource: IAPSource.googleplay,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(response.expiryTimeMillis ?? '0'),
),
);
// Update the database
if (userId != null) {
// If we know the userId,
// update the existing purchase or create it if it does not exist.
await iapRepository.createOrUpdatePurchase(purchaseData);
} else {
// If we do not know the user id, a previous entry must already
// exist, and thus we'll only update it.
await iapRepository.updatePurchase(purchaseData);
}
return true;
} on ap.DetailedApiRequestError catch (e) {
print(
'Error on handle Subscription: $e\n'
'JSON: ${e.jsonResponse}',
);
} catch (e) {
print('Error on handle Subscription: $e\n');
}
return false;
}
}
Adicione o método a seguir para facilitar a análise de IDs de pedidos, além de dois métodos para analisar o status da compra.
lib/google_play_purchase_handler.dart
NonSubscriptionStatus _nonSubscriptionStatusFrom(int? state) {
return switch (state) {
0 => NonSubscriptionStatus.completed,
2 => NonSubscriptionStatus.pending,
_ => NonSubscriptionStatus.cancelled,
};
}
SubscriptionStatus _subscriptionStatusFrom(int? state) {
return switch (state) {
// Payment pending
0 => SubscriptionStatus.pending,
// Payment received
1 => SubscriptionStatus.active,
// Free trial
2 => SubscriptionStatus.active,
// Pending deferred upgrade/downgrade
3 => SubscriptionStatus.pending,
// Expired or cancelled
_ => SubscriptionStatus.expired,
};
}
/// If a subscription suffix is present (..#) extract the orderId.
String extractOrderId(String orderId) {
final orderIdSplit = orderId.split('..');
if (orderIdSplit.isNotEmpty) {
orderId = orderIdSplit[0];
}
return orderId;
}
Agora suas compras no Google Play vão ser verificadas e armazenadas no banco de dados.
Em seguida, vamos falar sobre as compras na App Store para iOS.
Verificar compras do iOS: implementar o gerenciador de compras
Para verificar compras com a App Store, existe um pacote Dart de terceiros chamado app_store_server_sdk
que facilita o processo.
Comece criando a instância ITunesApi
. Use a configuração do sandbox e ative a geração de registros para facilitar a depuração de erros.
lib/app_store_purchase_handler.dart
final _iTunesAPI = ITunesApi(
ITunesHttpClient(
ITunesEnvironment.sandbox(),
loggingEnabled: true,
),
);
Agora, ao contrário das APIs do Google Play, a App Store usa os mesmos endpoints de API para assinaturas e não assinaturas. Isso significa que você pode usar a mesma lógica para os dois processadores. Junte-as para que elas chamem a mesma implementação:
lib/app_store_purchase_handler.dart
@override
Future<bool> handleNonSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
@override
Future<bool> handleSubscription({
required String userId,
required ProductData productData,
required String token,
}) {
return handleValidation(userId: userId, token: token);
}
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
//..
}
Agora, implemente handleValidation
:
lib/app_store_purchase_handler.dart
/// Handle purchase validation.
Future<bool> handleValidation({
required String userId,
required String token,
}) async {
print('AppStorePurchaseHandler.handleValidation');
final response = await _iTunesAPI.verifyReceipt(
password: appStoreSharedSecret,
receiptData: token,
);
print('response: $response');
if (response.status == 0) {
final receipts = response.latestReceiptInfo ?? [];
for (final receipt in receipts) {
final product = productDataMap[receipt.productId];
if (product == null) {
print('Error: Unknown product: ${receipt.productId}');
continue;
}
switch (product.type) {
case ProductType.nonSubscription:
await iapRepository.createOrUpdatePurchase(NonSubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
status: NonSubscriptionStatus.completed,
));
break;
case ProductType.subscription:
await iapRepository.createOrUpdatePurchase(SubscriptionPurchase(
userId: userId,
productId: receipt.productId ?? '',
iapSource: IAPSource.appstore,
orderId: receipt.originalTransactionId ?? '',
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.originalPurchaseDateMs ?? '0')),
type: product.type,
expiryDate: DateTime.fromMillisecondsSinceEpoch(
int.parse(receipt.expiresDateMs ?? '0')),
status: SubscriptionStatus.active,
));
break;
}
}
return true;
} else {
print('Error: Status: ${response.status}');
return false;
}
}
Agora suas compras na App Store precisam ser verificadas e armazenadas no banco de dados.
Executar o back-end
Nesse ponto, você pode executar dart bin/server.dart
para exibir o endpoint /verifypurchase
.
$ dart bin/server.dart
Serving at http://0.0.0.0:8080
11. Acompanhar as compras
A maneira recomendada de acompanhar as compras dos usuários é no serviço de back-end. Isso ocorre porque o back-end pode responder a eventos da loja e, portanto, é menos propenso a encontrar informações desatualizadas devido ao armazenamento em cache e menos suscetível a adulteração.
Primeiro, configure o processamento de eventos de armazenamento no back-end com o back-end Dart que você está criando.
Processar eventos da loja no back-end
As lojas podem informar o back-end sobre todos os eventos de faturamento que ocorrem, como a renovação de assinaturas. Você pode processar esses eventos no back-end para manter as compras no seu banco de dados atualizadas. Nesta seção, configure isso para a Google Play Store e a App Store da Apple.
Processar eventos de faturamento do Google Play
O Google Play fornece eventos de faturamento usando o que eles chamam de tópico do Cloud Pub/Sub. Elas são basicamente filas de mensagens em que as mensagens podem ser publicadas e consumidas.
Como essa é uma funcionalidade específica do Google Play, inclua essa funcionalidade no GooglePlayPurchaseHandler
.
Comece abrindo lib/google_play_purchase_handler.dart
e adicionando a importação PubsubApi:
lib/google_play_purchase_handler.dart
import 'package:googleapis/pubsub/v1.dart' as pubsub;
Em seguida, transmita o PubsubApi
para o GooglePlayPurchaseHandler
e modifique o construtor da classe para criar um Timer
da seguinte maneira:
lib/google_play_purchase_handler.dart
class GooglePlayPurchaseHandler extends PurchaseHandler {
final ap.AndroidPublisherApi androidPublisher;
final IapRepository iapRepository;
final pubsub.PubsubApi pubsubApi; // new
GooglePlayPurchaseHandler(
this.androidPublisher,
this.iapRepository,
this.pubsubApi, // new
) {
// Poll messages from Pub/Sub every 10 seconds
Timer.periodic(Duration(seconds: 10), (_) {
_pullMessageFromPubSub();
});
}
O Timer
está configurado para chamar o método _pullMessageFromSubSub
a cada dez segundos. Você pode ajustar a duração de acordo com sua preferência.
Em seguida, crie o _pullMessageFromSubSub
lib/google_play_purchase_handler.dart
/// Process messages from Google Play
/// Called every 10 seconds
Future<void> _pullMessageFromPubSub() async {
print('Polling Google Play messages');
final request = pubsub.PullRequest(
maxMessages: 1000,
);
final topicName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
final pullResponse = await pubsubApi.projects.subscriptions.pull(
request,
topicName,
);
final messages = pullResponse.receivedMessages ?? [];
for (final message in messages) {
final data64 = message.message?.data;
if (data64 != null) {
await _processMessage(data64, message.ackId);
}
}
}
Future<void> _processMessage(String data64, String? ackId) async {
final dataRaw = utf8.decode(base64Decode(data64));
print('Received data: $dataRaw');
final dynamic data = jsonDecode(dataRaw);
if (data['testNotification'] != null) {
print('Skip test messages');
if (ackId != null) {
await _ackMessage(ackId);
}
return;
}
final dynamic subscriptionNotification = data['subscriptionNotification'];
final dynamic oneTimeProductNotification =
data['oneTimeProductNotification'];
if (subscriptionNotification != null) {
print('Processing Subscription');
final subscriptionId =
subscriptionNotification['subscriptionId'] as String;
final purchaseToken = subscriptionNotification['purchaseToken'] as String;
final productData = productDataMap[subscriptionId]!;
final result = await handleSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else if (oneTimeProductNotification != null) {
print('Processing NonSubscription');
final sku = oneTimeProductNotification['sku'] as String;
final purchaseToken =
oneTimeProductNotification['purchaseToken'] as String;
final productData = productDataMap[sku]!;
final result = await handleNonSubscription(
userId: null,
productData: productData,
token: purchaseToken,
);
if (result && ackId != null) {
await _ackMessage(ackId);
}
} else {
print('invalid data');
}
}
/// ACK Messages from Pub/Sub
Future<void> _ackMessage(String id) async {
print('ACK Message');
final request = pubsub.AcknowledgeRequest(
ackIds: [id],
);
final subscriptionName =
'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
await pubsubApi.projects.subscriptions.acknowledge(
request,
subscriptionName,
);
}
O código que você acabou de adicionar se comunica com o tópico do Pub/Sub do Google Cloud a cada dez segundos e solicita novas mensagens. Em seguida, processa cada mensagem no método _processMessage
.
Esse método decodifica as mensagens recebidas e recebe as informações atualizadas sobre cada compra, tanto de assinaturas quanto de não assinaturas, chamando o handleSubscription
ou handleNonSubscription
, se necessário.
Cada mensagem precisa ser confirmada com o método _askMessage
.
Em seguida, adicione as dependências necessárias ao arquivo server.dart
. Adicione o PubsubApi.cloudPlatformScope à configuração de credenciais:
bin/server.dart
final clientGooglePlay =
await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
ap.AndroidPublisherApi.androidpublisherScope,
pubsub.PubsubApi.cloudPlatformScope, // new
]);
Em seguida, crie a instância da PubsubApi:
bin/server.dart
final pubsubApi = pubsub.PubsubApi(clientGooglePlay);
E, por fim, transmita para o construtor GooglePlayPurchaseHandler
:
bin/server.dart
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi, // new
),
'app_store': AppStorePurchaseHandler(
iapRepository,
),
};
Configuração do Google Play
Você escreveu o código para consumir eventos de faturamento do tópico do Pub/Sub, mas não criou o tópico nem está publicando eventos de faturamento. É hora de configurar.
Primeiro, crie um tópico do Pub/Sub:
- Acesse a página do Cloud Pub/Sub no console do Google Cloud.
- Verifique se você está no seu projeto do Firebase e clique em + Criar tópico.
- Dê um nome ao novo tópico idêntico ao valor definido para
GOOGLE_PLAY_PUBSUB_BILLING_TOPIC
emconstants.ts
. Neste caso, nomeie-o comoplay_billing
. Se você escolher outra opção, atualizeconstants.ts
. Crie o tópico. - Na lista de tópicos do Pub/Sub, clique nos três pontos verticais do tópico que você acabou de criar e em Ver permissões.
- Na barra lateral à direita, escolha Adicionar principal.
- Aqui, adicione
google-play-developer-notifications@system.gserviceaccount.com
e conceda a ele a função de Editor do Pub/Sub. - Salve as mudanças de permissão.
- Copie o nome do tópico que você acabou de criar.
- Abra o Play Console novamente e escolha seu app na lista Todos os apps.
- Role para baixo e acesse Monetização > Configuração de monetização.
- Preencha o tópico completo e salve as alterações.
Todos os eventos de faturamento do Google Play serão publicados no tópico.
Processar eventos de faturamento da App Store
Em seguida, faça o mesmo para os eventos de faturamento da App Store. Há duas maneiras eficazes de implementar o processamento de atualizações em compras na App Store. Uma delas é implementar um webhook que você fornece à Apple e que ela usa para se comunicar com seu servidor. A segunda maneira, que é a que você vai encontrar neste codelab, é se conectar à API App Store Server e receber as informações de assinatura manualmente.
Este codelab se concentra na segunda solução porque você precisa expor seu servidor à Internet para implementar o webhook.
Em um ambiente de produção, o ideal é ter os dois. O webhook para receber eventos da App Store e a API Server caso você perca um evento ou precise verificar o status de uma assinatura.
Comece abrindo lib/app_store_purchase_handler.dart
e adicionando a dependência AppStoreServerAPI:
lib/app_store_purchase_handler.dart
final AppStoreServerAPI appStoreServerAPI;
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI, // new
)
Modifique o construtor para adicionar um timer que chame o método _pullStatus
. Esse timer vai chamar o método _pullStatus
a cada 10 segundos. É possível ajustar a duração do timer de acordo com suas necessidades.
lib/app_store_purchase_handler.dart
AppStorePurchaseHandler(
this.iapRepository,
this.appStoreServerAPI,
) {
// Poll Subscription status every 10 seconds.
Timer.periodic(Duration(seconds: 10), (_) {
_pullStatus();
});
}
Em seguida, crie o método _pullStatus da seguinte maneira:
lib/app_store_purchase_handler.dart
Future<void> _pullStatus() async {
print('Polling App Store');
final purchases = await iapRepository.getPurchases();
// filter for App Store subscriptions
final appStoreSubscriptions = purchases.where((element) =>
element.type == ProductType.subscription &&
element.iapSource == IAPSource.appstore);
for (final purchase in appStoreSubscriptions) {
final status =
await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
// Obtain all subscriptions for the order id.
for (final subscription in status.data) {
// Last transaction contains the subscription status.
for (final transaction in subscription.lastTransactions) {
final expirationDate = DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.expiresDate ?? 0);
// Check if subscription has expired.
final isExpired = expirationDate.isBefore(DateTime.now());
print('Expiration Date: $expirationDate - isExpired: $isExpired');
// Update the subscription status with the new expiration date and status.
await iapRepository.updatePurchase(SubscriptionPurchase(
userId: null,
productId: transaction.transactionInfo.productId,
iapSource: IAPSource.appstore,
orderId: transaction.originalTransactionId,
purchaseDate: DateTime.fromMillisecondsSinceEpoch(
transaction.transactionInfo.originalPurchaseDate),
type: ProductType.subscription,
expiryDate: expirationDate,
status: isExpired
? SubscriptionStatus.expired
: SubscriptionStatus.active,
));
}
}
}
}
Esse método funciona da seguinte maneira:
- Recebe a lista de assinaturas ativas do Firestore usando o IapRepository.
- Para cada pedido, ele solicita o status da assinatura para a API App Store Server.
- Recebe a última transação dessa compra de assinatura.
- Verifica a data de validade.
- Atualiza o status da assinatura no Firestore. Se ela tiver expirado, será marcada como tal.
Por fim, adicione todo o código necessário para configurar o acesso à API App Store Server:
bin/server.dart
// add from here
final subscriptionKeyAppStore =
File('assets/SubscriptionKey.p8').readAsStringSync();
// Configure Apple Store API access
var appStoreEnvironment = AppStoreEnvironment.sandbox(
bundleId: bundleId,
issuerId: appStoreIssuerId,
keyId: appStoreKeyId,
privateKey: subscriptionKeyAppStore,
);
// Stored token for Apple Store API access, if available
final file = File('assets/appstore.token');
String? appStoreToken;
if (file.existsSync() && file.lengthSync() > 0) {
appStoreToken = file.readAsStringSync();
}
final appStoreServerAPI = AppStoreServerAPI(
AppStoreServerHttpClient(
appStoreEnvironment,
jwt: appStoreToken,
jwtTokenUpdatedCallback: (token) {
file.writeAsStringSync(token);
},
),
);
// to here
return {
'google_play': GooglePlayPurchaseHandler(
androidPublisher,
iapRepository,
pubsubApi,
),
'app_store': AppStorePurchaseHandler(
iapRepository,
appStoreServerAPI, // new
),
};
Configuração da App Store
Em seguida, configure a App Store:
- Faça login no App Store Connect e selecione Usuários e acesso.
- Acesse Tipo de chave > Compra no app.
- Toque no ícone de adição para adicionar um novo.
- Dê um nome, por exemplo, "Chave do Codelab".
- Faça o download do arquivo p8 que contém a chave.
- Copie-o para a pasta "assets" com o nome
SubscriptionKey.p8
. - Copie o ID da chave recém-criada e defina-o como constante
appStoreKeyId
no arquivolib/constants.dart
. - Copie o ID do emissor na parte de cima da lista de chaves e defina-o como constante
appStoreIssuerId
no arquivolib/constants.dart
.
Rastrear compras no dispositivo
A maneira mais segura de acompanhar suas compras é do lado do servidor, porque o cliente é difícil de proteger. No entanto, é preciso ter uma forma de enviar as informações de volta ao cliente para que o app possa agir com base nas informações de status da assinatura. Ao armazenar as compras no Firestore, você pode sincronizar facilmente os dados com o cliente e mantê-los atualizados automaticamente.
Você já incluiu o IAPRepo no app, que é o repositório do Firestore que contém todos os dados de compra do usuário em List<PastPurchase> purchases
. O repositório também contém hasActiveSubscription,
, que é verdadeiro quando há uma compra com productId storeKeySubscription
com um status que não expirou. Quando o usuário não está conectado, a lista fica vazia.
lib/repo/iap_repo.dart
void updatePurchases() {
_purchaseSubscription?.cancel();
var user = _user;
if (user == null) {
purchases = [];
hasActiveSubscription = false;
hasUpgrade = false;
return;
}
var purchaseStream = _firestore
.collection('purchases')
.where('userId', isEqualTo: user.uid)
.snapshots();
_purchaseSubscription = purchaseStream.listen((snapshot) {
purchases = snapshot.docs.map((DocumentSnapshot document) {
var data = document.data();
return PastPurchase.fromJson(data);
}).toList();
hasActiveSubscription = purchases.any((element) =>
element.productId == storeKeySubscription &&
element.status != Status.expired);
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
notifyListeners();
});
}
Toda a lógica de compra está na classe DashPurchases
e é onde as assinaturas precisam ser aplicadas ou removidas. Portanto, adicione o iapRepo
como uma propriedade na classe e atribua o iapRepo
no construtor. Em seguida, adicione um listener diretamente no construtor e remova o listener no método dispose()
. No início, o listener pode ser apenas uma função vazia. Como o IAPRepo
é um ChangeNotifier
e você chama notifyListeners()
toda vez que as compras no Firestore mudam, o método purchasesUpdate()
é sempre chamado quando os produtos comprados mudam.
lib/logic/dash_purchases.dart
IAPRepo iapRepo;
DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
final purchaseUpdated = iapConnection.purchaseStream;
_subscription = purchaseUpdated.listen(
_onPurchaseUpdate,
onDone: _updateStreamOnDone,
onError: _updateStreamOnError,
);
iapRepo.addListener(purchasesUpdate);
loadPurchases();
}
@override
void dispose() {
_subscription.cancel();
iapRepo.removeListener(purchasesUpdate);
super.dispose();
}
void purchasesUpdate() {
//TODO manage updates
}
Em seguida, forneça o IAPRepo
ao construtor em main.dart.
. Você pode acessar o repositório usando context.read
, porque ele já foi criado em um Provider
.
lib/main.dart
ChangeNotifierProvider<DashPurchases>(
create: (context) => DashPurchases(
context.read<DashCounter>(),
context.read<FirebaseNotifier>(),
context.read<IAPRepo>(),
),
lazy: false,
),
Em seguida, escreva o código para a função purchaseUpdate()
. Em dash_counter.dart,
, os métodos applyPaidMultiplier
e removePaidMultiplier
definem o multiplicador como 10 ou 1, respectivamente, para que você não precise verificar se a assinatura já foi aplicada. Quando o status da assinatura muda, você também atualiza o status do produto que pode ser comprado para mostrar na página de compra que ele já está ativo. Defina a propriedade _beautifiedDashUpgrade
com base na compra do upgrade.
lib/logic/dash_purchases.dart
void purchasesUpdate() {
var subscriptions = <PurchasableProduct>[];
var upgrades = <PurchasableProduct>[];
// Get a list of purchasable products for the subscription and upgrade.
// This should be 1 per type.
if (products.isNotEmpty) {
subscriptions = products
.where((element) => element.productDetails.id == storeKeySubscription)
.toList();
upgrades = products
.where((element) => element.productDetails.id == storeKeyUpgrade)
.toList();
}
// Set the subscription in the counter logic and show/hide purchased on the
// purchases page.
if (iapRepo.hasActiveSubscription) {
counter.applyPaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchased);
}
} else {
counter.removePaidMultiplier();
for (var element in subscriptions) {
_updateStatus(element, ProductStatus.purchasable);
}
}
// Set the Dash beautifier and show/hide purchased on
// the purchases page.
if (iapRepo.hasUpgrade != _beautifiedDashUpgrade) {
_beautifiedDashUpgrade = iapRepo.hasUpgrade;
for (var element in upgrades) {
_updateStatus(
element,
_beautifiedDashUpgrade
? ProductStatus.purchased
: ProductStatus.purchasable);
}
notifyListeners();
}
}
void _updateStatus(PurchasableProduct product, ProductStatus status) {
if (product.status != ProductStatus.purchased) {
product.status = ProductStatus.purchased;
notifyListeners();
}
}
Agora você garantiu que o status da assinatura e do upgrade esteja sempre atualizado no serviço de back-end e sincronizado com o app. O app age de acordo e aplica os recursos de assinatura e upgrade ao jogo de cliques do Dash.
12. Pronto!
Parabéns! Você concluiu o codelab. O código completo deste codelab está na pasta complete.
Para saber mais, veja os outros codelabs do Flutter (link em inglês).