Como adicionar compras ao seu app do Flutter

1. Introdução

Para adicionar compras no app a um app Flutter, é necessário configurar corretamente a App Store e a Play Store, verificar a compra e conceder as permissões necessárias, como benefícios de assinatura.

Neste codelab, você vai adicionar três tipos de compras no app a um aplicativo (fornecido para você) 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:

  1. Uma opção de compra repetível para 2.000 Dashes de uma só vez.
  2. Uma compra única de upgrade para transformar o Dash de estilo antigo em um Dash de estilo moderno.
  3. 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. Ela só precisa ser comprada uma vez e 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 traços mais rapidamente, mas quando ele parar de pagar, os benefícios também vão acabar.

O serviço de back-end (também fornecido) é 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.

300123416ebc8dc1.png 7145d0fffe6ea741.png 646317a79be08214.png

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 do Dart para verificar e armazenar os itens comprados.

O que você vai aprender

  • Como configurar a App Store e a Play Store com produtos disponíveis para compra.
  • Como se comunicar com as lojas para verificar e armazenar compras no Firestore.
  • Como gerenciar compras no seu app.

O que é necessário

  • Android Studio
  • Xcode (para desenvolvimento no iOS)
  • SDK do Flutter (em inglês)

2. Configurar o ambiente de desenvolvimento

Para começar 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 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 exemplo de código é 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ê deve estar no final de cada etapa nomeada. O código inicial está na etapa 0. Navegue até ele da seguinte maneira:

cd flutter-codelabs/in_app_purchases/step_00

Se quiser avançar ou ver como algo deve ficar após uma etapa, procure no diretório com o nome da etapa em que você tem interesse. O código da última etapa está na pasta complete.

Configurar o projeto inicial

Abra o projeto inicial de step_00/app 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. Com 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 do iOS, ele é chamado de identificador do pacote, e na Play Store do Android, de ID do aplicativo. Esses identificadores geralmente são criados usando uma notação de nome de domínio invertido. Por exemplo, ao fazer uma compra no app para flutter.dev, você usaria dev.flutter.inapppurchase. Pense em um identificador para seu app e defina-o nas configurações do projeto.

Primeiro, configure o identificador do pacote para iOS. Para isso, abra o arquivo Runner.xcworkspace no app Xcode.

a9fbac80a31e28e0.png

Na estrutura de pastas do Xcode, o projeto Runner fica na parte de cima, e os destinos Flutter, Runner e Products estão abaixo dele. 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.

812f919d965c649a.jpeg

Agora você pode fechar o Xcode e voltar para o Android Studio para concluir a configuração do Android. Para fazer isso, abra o arquivo build.gradle.kts em android/app, e mude o applicationId (na linha 24 da captura de tela abaixo) para o ID do aplicativo, o mesmo que o identificador do pacote do iOS. Os IDs das lojas iOS e Android não precisam ser idênticos, mas é menos provável que haja erros se eles forem iguais. Por isso, neste codelab, também vamos usar identificadores idênticos.

e320a49ff2068ac2.png

3. Instalar o plug-in

Nesta parte do codelab, você vai instalar o plug-in in_app_purchase.

Adicionar dependência em pubspec

Adicione in_app_purchase ao pubspec adicionando in_app_purchase às dependências do projeto:

$ cd app
$ flutter pub add in_app_purchase dev:in_app_purchase_platform_interface
Resolving dependencies... 
Downloading packages... 
  characters 1.4.0 (1.4.1 available)
  flutter_lints 5.0.0 (6.0.0 available)
+ in_app_purchase 3.2.3
+ in_app_purchase_android 0.4.0+3
+ in_app_purchase_platform_interface 1.4.0
+ in_app_purchase_storekit 0.4.4
+ json_annotation 4.9.0
  lints 5.1.1 (6.0.0 available)
  material_color_utilities 0.11.1 (0.13.0 available)
  meta 1.16.0 (1.17.0 available)
  provider 6.1.5 (6.1.5+1 available)
  test_api 0.7.6 (0.7.7 available)
Changed 5 dependencies!
7 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Abra seu pubspec.yaml e confirme se agora você tem in_app_purchase listado como uma entrada em dependencies e in_app_purchase_platform_interface em dev_dependencies.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cloud_firestore: ^6.0.0
  cupertino_icons: ^1.0.8
  firebase_auth: ^6.0.1
  firebase_core: ^4.0.0
  google_sign_in: ^7.1.1
  http: ^1.5.0
  intl: ^0.20.2
  provider: ^6.1.5
  logging: ^1.3.0
  in_app_purchase: ^3.2.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0
  in_app_purchase_platform_interface: ^1.4.0

4. Configurar a App Store

Para configurar e testar compras no app no iOS, crie um app na App Store e produtos compráveis nele. Não é necessário publicar nada nem enviar o app para revisão da Apple. Você precisa de uma conta de desenvolvedor para fazer isso. Se você não tiver uma, inscreva-se no programa de desenvolvedores da Apple.

Para usar 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 Contratos, tributos e serviços bancários.

11db9fca823e7608.png

Aqui você encontra os contratos de apps sem custo financeiro e pagos. O status dos apps sem custo financeiro precisa estar ativo, e o status dos apps pagos é "novo". Leia e aceite os termos e insira todas as informações necessárias.

74c73197472c9aec.png

Quando tudo estiver configurado corretamente, o status dos apps pagos vai estar ativo. Isso é muito importante porque não é possível testar compras no app sem um contrato ativo.

4a100bbb8cafdbbf.jpeg

Registrar ID do app

Crie um novo identificador no portal de desenvolvedores da Apple. Acesse developer.apple.com/account/resources/identifiers/list e clique no ícone de adição ao lado do cabeçalho Identificadores.

55d7e592d9a3fc7b.png

Escolher IDs de apps

13f125598b72ca77.png

Selecione um app

41ac4c13404e2526.png

Forneça uma descrição e defina o ID do pacote para corresponder ao mesmo valor definido anteriormente no Xcode.

9d2c940ad80deeef.png

Para mais orientações sobre como criar um ID de app, consulte a Ajuda da conta de desenvolvedor.

Criar um novo app

Crie um app no App Store Connect com seu identificador de pacote exclusivo.

10509b17fbf031bd.png

5b7c0bb684ef52c7.png

Para mais orientações sobre como criar um novo 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 Sandbox para criar uma conta de sandbox ou gerenciar os IDs Apple de sandbox atuais.

2ba0f599bcac9b36.png

Agora você pode configurar o usuário de sandbox no iPhone acessando Ajustes > Desenvolvedor > Conta Apple de sandbox.

74a545210b282ad8.png eaa67752f2350f74.png

Configurar as compras no app

Agora, configure os três itens disponíveis para compra:

  • dash_consumable_2k: uma compra consumível que pode ser feita várias vezes e concede ao usuário 2.000 Dashes (a moeda do app) por compra.
  • dash_upgrade_3d: uma compra "upgrade" não consumível que só pode ser feita uma vez e dá ao usuário um Dash com aparência diferente para clicar.
  • dash_subscription_doubler: uma assinatura que concede ao usuário o dobro de traços por clique durante a vigência da assinatura.

a118161fac83815a.png

Acesse Compras no app.

Crie suas compras no app com os IDs especificados:

  1. 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. Defina como dash consumable 2k. 1f8527fc03902099.png Defina a disponibilidade. O produto precisa estar disponível no país do usuário do sandbox. bd6b2ce2d9314e6e.png Adicione o preço e defina-o como $1.99 ou o equivalente em outra moeda. 926b03544ae044c4.png Adicione suas localizações para a compra. Chame a compra Spring is in the air com 2000 dashes fly out como descrição. e26dd4f966dcfece.png Adicione uma captura de tela da avaliação. O conteúdo não importa, a menos que o produto seja enviado para revisão, mas é necessário para que ele esteja no estado "Pronto para envio", o que é necessário quando o app busca produtos na App Store. 25171bfd6f3a033a.png
  2. Configure dash_upgrade_3d como um não consumível. Use dash_upgrade_3d como o ID do produto. Defina o nome da referência como dash upgrade 3d. Chame a compra 3D Dash com Brings your dash back to the future como descrição. Defina o preço como $0.99. Configure a disponibilidade e faça upload da captura de tela da avaliação da mesma forma que para o produto dash_consumable_2k. 83878759f32a7d4a.png
  3. Configure dash_subscription_doubler como uma assinatura com renovação automática. O fluxo para assinaturas é um pouco diferente. Primeiro, crie um grupo de assinaturas. Quando várias assinaturas fazem parte do mesmo grupo, um usuário só pode assinar uma delas por vez, mas pode fazer upgrade ou downgrade entre elas. Basta chamar este grupo de subscriptions. 393a44b09f3cd8bf.png E adicione a localização para o grupo de assinaturas. 595aa910776349bd.png Em seguida, crie a assinatura. Defina o nome de referência como dash subscription doubler e o ID do produto como dash_subscription_doubler. 7bfff7bbe11c8eec.png Em seguida, selecione a duração da assinatura de uma semana e as localizações. Nomeie esta assinatura como Jet Engine com a descrição Doubles your clicks. Defina o preço como $0.49. Configure a disponibilidade e faça upload da captura de tela da avaliação da mesma forma que para o produto dash_consumable_2k. 44d18e02b926a334.png

Os produtos vão aparecer nas listas:

17f242b5c1426b79.png d71da951f595054a.png

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 tiver uma, registre uma conta.

Criar um novo app

Crie um app no Google Play Console:

  1. Abra o Play Console.
  2. Selecione Todos os apps > Criar app.
  3. 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. Você pode mudar o nome depois.
  4. Especifique que o aplicativo é um jogo. Isso pode ser alterado mais tarde.
  5. Especifique se o app é sem custo financeiro ou pago.
  6. Preencha as declarações "Diretrizes de conteúdo" e "Leis de exportação dos EUA".
  7. 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 do conteúdo e capturas de tela. 13845badcf9bc1db.png

Assinar o aplicativo

Para testar compras no app, é preciso ter pelo menos um build enviado ao Google Play.

Para isso, é necessário que o build de lançamento seja assinado com algo diferente das chaves de depuração.

Criar um keystore

Se você já tiver um keystore, pule para a próxima etapa. Se não houver, 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 diretório principal. Se quiser armazenar o arquivo em outro lugar, mude o argumento transmitido ao parâmetro -keystore. Mantenha o

keystore

mantenha o arquivo privado e não o registre em um controle de origem público.

Referenciar a keystore no app

Crie um arquivo chamado <your app dir>/android/key.properties que contenha uma referência ao seu 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.kts.

Adicione as informações do keystore do arquivo de propriedades antes do bloco android:

import java.util.Properties
import java.io.FileInputStream

plugins {
    // omitted
}

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // omitted
}

Carregue o arquivo key.properties no objeto keystoreProperties.

Atualize o bloco buildTypes para:

   buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

Configure o bloco signingConfigs no arquivo build.gradle.kts do módulo com as informações de configuração de assinatura:

   signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }

Os builds de lançamento do app agora serão assinados automaticamente.

Para mais informações sobre como assinar seu app, consulte Assinar o app em developer.android.com.

Fazer upload da primeira build

Depois que o app estiver configurado para assinatura, você poderá 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 Testar e lançar > Teste > Teste fechado e crie uma nova versão de teste fechado.

Em seguida, faça upload do pacote de apps app-release.aab gerado pelo comando de build.

Clique em Salvar e em Analisar lançamento.

Por fim, clique em Iniciar o lançamento para teste fechado para ativar a versão de teste fechado.

Configurar usuários de teste

Para testar compras no app, as Contas do Google dos testadores precisam ser adicionadas ao Google Play Console em dois locais:

  1. Para a faixa de teste específica (teste interno)
  2. Como testador de licença

Primeiro, adicione o testador à faixa de teste interno. Volte para Testar e lançar > Testes > Teste interno e clique na guia Testadores.

a0d0394e85128f84.png

Clique em Criar lista de e-mails. Dê um nome à lista e adicione os endereços de e-mail das Contas do Google que precisam de acesso aos testes de 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:

  1. Volte para a visualização Todos os apps do Google Play Console.
  2. Acesse Configurações > Teste de licença.
  3. Adicione os mesmos endereços de e-mail dos testadores que precisam testar as compras no app.
  4. Defina Resposta da licença como RESPOND_NORMALLY.
  5. Clique em Salvar alterações.

a1a0f9d3e55ea8da.png

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 e concede ao usuário 2.000 Dashes (a moeda do app) por compra.
  • dash_upgrade_3d: uma compra "upgrade" não consumível que só pode ser feita uma vez e dá ao usuário um Dash com uma aparência diferente para clicar.
  • dash_subscription_doubler: uma assinatura que concede ao usuário o dobro de traços por clique durante a vigência da assinatura.

Primeiro, adicione o consumível e o não consumível.

  1. Acesse o Google Play Console e selecione seu aplicativo.
  2. Acesse Monetização > Produtos > Produtos no app.
  3. Clique em Criar produtoc8d66e32f57dee21.png.
  4. Insira todas as informações necessárias sobre seu produto. Verifique se o ID do produto corresponde exatamente ao ID que você pretende usar.
  5. Clique em Salvar.
  6. Clique em Ativar.
  7. Repita o processo para a compra não consumível de "upgrade".

Em seguida, adicione a assinatura:

  1. Acesse o Google Play Console e selecione seu aplicativo.
  2. Acesse Monetização > Produtos > Assinaturas.
  3. Clique em Criar assinatura32a6a9eefdb71dd0.png.
  4. Insira todas as informações necessárias para sua assinatura. Verifique se o ID do produto corresponde exatamente ao ID que você pretende usar.
  5. 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.

Usar um serviço de back-end tem vários benefícios:

  • Você pode verificar transações com segurança.
  • Você pode reagir a eventos de faturamento das app stores.
  • Você pode acompanhar as compras em um banco de dados.
  • Os usuários não poderão enganar seu app para fornecer recursos premium rebobinando o relógio do sistema.

Embora haja muitas maneiras de configurar um serviço de back-end, você fará isso usando o Cloud Functions e o Firestore, com o Firebase do Google.

A programaçã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 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ário fazer a autenticação. Para isso, use o módulo de autenticação do Firebase com o login do Google.

  1. No painel do Firebase, acesse Autenticação e ative-a, se necessário.
  2. Acesse a guia Método de login e ative o provedor de login do Google.

fe2e0933d6810888.png

Como você também vai usar o banco de dados do Firestore do Firebase, ative-o também.

d02d641821c71e2c.png

Defina as 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 "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 o iOS e o Android selecionando as duas plataformas.

? Which platforms should your configuration support (use arrow keys & space to select)? ›
✔ android
✔ ios
  macos
  web

Quando for 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 o app dashclicker (android).

b22d46a759c0c834.png

Para permitir o Login do Google no modo de depuração, forneça a impressão digital do hash SHA-1 do certificado de depuração.

Receber 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

Uma grande lista de chaves de assinatura vai aparecer. 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.

Por fim, execute o comando flutterfire configure novamente para atualizar o app e incluir a configuração de assinatura.

$ flutterfire configure
? You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the valus in your existing `firebase.json` file to configure your project? (y/n) › yes
✔ You have an existing `firebase.json` file and possibly already configured your project for Firebase. Would you prefer to reuse the values in your existing `firebase.json` file to configure your project? · yes

Configurar o Firebase para iOS: outras etapas

Abra o ios/Runner.xcworkspace com Xcode. Ou com o ambiente de desenvolvimento integrado de sua escolha.

No VSCode, clique com o botão direito do mouse na pasta ios/ e 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 de build. Consulte a documentação do pacote google_sign_in para mais informações. Nesse caso, o arquivo é ios/Runner/Info.plist.

O par de chave-valor já foi adicionado, mas os valores precisam ser substituídos:

  1. Receba o valor de REVERSED_CLIENT_ID do arquivo GoogleService-Info.plist, sem o elemento <string>..</string> ao redor.
  2. Substitua o valor no arquivo ios/Runner/Info.plist na chave CFBundleURLTypes.
<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 compras

Nesta parte do codelab, você vai preparar o app para comprar os produtos. Esse processo inclui ouvir atualizações e erros de compra depois que o app é iniciado.

Ouvir atualizações de compras

Em main.dart,, encontre o widget MyHomePage que tem um Scaffold com um BottomNavigationBar contendo duas páginas. Essa página também cria três Providers para DashCounter, DashUpgrades, e DashPurchases. DashCounter rastreia a contagem atual de traços e os incrementa automaticamente. DashUpgrades gerencia os upgrades que você pode comprar com Dashes. Este codelab se concentra em DashPurchases.

Por padrão, o objeto de um provedor é definido quando ele é solicitado pela primeira vez. Esse objeto fica atento às atualizações de compra diretamente quando o app é iniciado. Portanto, desative o carregamento lento nele 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!;
  }
}

Atualize o teste da seguinte maneira:

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 da DashPurchasesChangeNotifier. No momento, só é possível adicionar um DashCounter aos painéis comprados.

Adicione uma propriedade de assinatura de stream, _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 _subscription é inicializado no construtor. Esse projeto é configurado para ser não anulável por padrão (NNBD), o que significa que as propriedades não declaradas como anuláveis precisam ter um valor não nulo. O qualificador late permite atrasar a definição desse valor.

No construtor, receba o stream purchaseUpdated e comece a detectar o stream. No método dispose(), cancele a assinatura do stream.

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';

import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.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 simulados 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 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 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, definir storeState como notAvailable mostra uma mensagem de erro ao 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 da Google Play e da App Store, você verá storeKeyConsumable, storeKeySubscription, e storeKeyUpgrade. Quando uma compra esperada não estiver disponível, imprima essas informações no console. Também é possível enviar essas informações para o serviço de back-end.

O método await iapConnection.queryProductDetails(ids) retorna os IDs não encontrados e os produtos disponíveis para compra que foram encontrados. 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();                                       // Add this line
  }

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 disponíveis para compra

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 serão usadas na próxima etapa.

O widget _PurchaseList mostra a lista de produtos disponíveis para compra 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(),
    );
  }
}

Se estiverem configurados corretamente, os produtos disponíveis vão aparecer nas lojas Android e iOS. Pode levar algum tempo para que as compras fiquem disponíveis quando inseridas nos consoles respectivos.

ca1a9f97c21e552d.png

Volte para dash_purchases.dart e implemente a função para comprar um produto. Basta separar os 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 rastrear e verificar compras, configure um back-end do Dart para oferecer suporte a isso.

Nesta seção, trabalhe na pasta dart-backend/ como raiz.

Verifique se você tem as seguintes ferramentas instaladas:

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 usar. No entanto, é preciso se conectar do 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 veicular 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 é o IapRepository em lib/iap_repository.dart. Como aprender a interagir com o Firestore ou bancos de dados em geral não é considerado relevante para este codelab, o código inicial contém funções para você criar ou atualizar compras no Firestore, bem como todas as classes para essas compras.

Configurar o acesso ao Firebase

Para acessar o Firebase Firestore, você precisa de uma chave de acesso da conta de serviço. Para gerar uma, abra as configurações do projeto do Firebase, navegue até a seção Contas de serviço e selecione Gerar nova chave privada.

27590fc77ae94ad4.png

Copie o arquivo JSON baixado para a pasta assets/ e renomeie como service-account-firebase.json.

Configurar o acesso ao Google Play

Para acessar a Play Store e verificar compras, gere uma conta de serviço com essas permissões e faça o download das credenciais JSON dela.

  1. Acesse a página da API Google Play Android Developer no console do Google Cloud. 629f0bd8e6b50be8.png Se o Google Play Console pedir que você crie ou vincule um projeto, faça isso primeiro e depois volte a esta página.
  2. Em seguida, acesse a página "Contas de serviço" e clique em + Criar conta de serviço. 8dc97e3b1262328a.png
  3. Insira o Nome da conta de serviço e clique em Criar e continuar. 4fe8106af85ce75f.png
  4. Selecione a função Assinante do Pub/Sub e clique em Concluído. a5b6fa6ea8ee22d.png
  5. Depois que a conta for criada, acesse Gerenciar chaves. eb36da2c1ad6dd06.png
  6. Selecione Adicionar chave > Criar nova chave. e92db9557a28a479.png
  7. Crie e faça o download de uma chave JSON. 711d04f2f4176333.png
  8. Renomeie o arquivo baixado como service-account-google-play.json, e mova-o para o diretório assets/.
  9. Em seguida, acesse a página Usuários e permissões no Play Console28fffbfc35b45f97.png.
  10. Clique em Convidar novos usuários e insira o endereço de e-mail da conta de serviço criada anteriormente. Você pode encontrar o e-mail na tabela da página "Contas de serviço"e3310cc077f397d.png.
  11. Conceda as permissões Ver dados financeiros e Gerenciar pedidos e assinaturas para o aplicativo. a3b8cf2b660d1900.png
  12. Clique em Convidar usuário.

Outra 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 à App Store da Apple

Para acessar a App Store e verificar compras, você precisa configurar uma senha secreta compartilhada:

  1. Abra o App Store Connect.
  2. Acesse Meus apps e selecione seu app.
  3. Na navegação da barra lateral, acesse Geral > Informações do app.
  4. Clique em Gerenciar no cabeçalho Segredo compartilhado específico do app. ad419782c5fbacb2.png
  5. Gere um novo secret e copie-o. b5b72a357459b0e5.png
  6. Abra lib/constants.dart, e substitua o valor de appStoreSharedSecret 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, como com.example.dashclicker
  • appStoreSharedSecret: senha secreta compartilhada para acessar o App Store Connect e fazer a verificação de compras.
  • bundleId: ID do pacote usado no iOS, como com.example.dashclicker

Por enquanto, você pode ignorar o restante das constantes.

10. Verificar compras

O fluxo geral para verificar compras é semelhante para iOS e Android.

Em ambas as lojas, seu aplicativo recebe um token quando uma compra é feita.

Esse token é enviado pelo app ao seu serviço de back-end, que, por sua vez, verifica a compra com os servidores da loja correspondente 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 realize a validação com as lojas em vez do aplicativo em execução no dispositivo do usuário, você pode impedir que ele tenha acesso a recursos premium, por exemplo, voltando 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, verifique se o usuário está autenticado ao fazer uma compra. A maior parte da lógica de autenticação já está adicionada para você no projeto inicial. Basta garantir que o PurchasePage mostre o botão de login quando o usuário ainda não estiver conectado. Adicione o seguinte código ao início do método de build de PurchasePage:

lib/pages/purchase_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../logic/dash_purchases.dart';
import '../logic/firebase_notifier.dart';                  // Add this import
import '../model/firebase_state.dart';                     // And this import
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import '../repo/iap_repo.dart';
import 'login_page.dart';                                  // And this one as well

class PurchasePage extends StatelessWidget {
  const PurchasePage({super.key});

  @override
  Widget build(BuildContext context) {                     // Update from here
    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();
    }                                                      // To here.

    // ...

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 um 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 sua máquina local.

lib/logic/dash_purchases.dart

import 'dart:async';
import 'dart:convert';                                     // Add this import

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;                   // And this import
import 'package:in_app_purchase/in_app_purchase.dart';

import '../constants.dart';
import '../main.dart';
import '../model/purchasable_product.dart';
import '../model/store_state.dart';
import 'dash_counter.dart';
import 'firebase_notifier.dart';                           // And this one

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;                       // Add this line
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;

  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter, this.firebaseNotifier) {     // Update this line
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    loadPurchases();
  }

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 que você possa transmitir o ID do usuário à função de verificação de compra.

lib/logic/firebase_notifier.dart

  Future<FirebaseFirestore> get firestore async {
    var isInitialized = await _isInitialized.future;
    if (!isInitialized) {
      throw Exception('Firebase is not initialized');
    }
    return FirebaseFirestore.instance;
  }

  User? get user => FirebaseAuth.instance.currentUser;     // Add this line

  Future<void> load() async {
    // ...

Adicione a função _verifyPurchase à classe DashPurchases. Essa função async retorna um booleano que indica 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 logo antes de aplicar a compra. Só aplique a compra quando ela for verificada. Em um app de produção, é possível especificar isso ainda mais para, por exemplo, aplicar uma assinatura de teste quando a loja estiver temporariamente indisponível. No entanto, para este exemplo, 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(2000);
          case storeKeyUpgrade:
            _beautifiedDashUpgrade = true;
        }
      }
    }

    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 o back-end para verificar compras.

Criar manipuladores de compras

Como o fluxo de verificação das duas lojas é quase idêntico, configure uma classe abstrata PurchaseHandler com implementações separadas para cada loja.

be50c207c5a2a519.png

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 (link em inglês)

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 compras ao usuário.
  • productData: Dados sobre o produto. Você vai definir isso em um minuto.
  • token: O token fornecido ao usuário pela loja.

Além disso, para facilitar o uso desses manipuladores de compras, adicione um método verifyPurchase() que pode ser usado para assinaturas e não assinaturas:

lib/purchase_handler.dart (link em inglês)

  /// 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 os dois casos, mas ainda ter implementações separadas.

A classe ProductData contém informações básicas sobre os diferentes produtos disponíveis para compra, 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 Apple App Store. 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 manipulador. Você vai chegar a eles mais tarde.

Como você deve ter notado, o construtor usa uma instância do IapRepository. O manipulador 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 manipulador 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,
  }) async {
    return true;
  }

  @override
  Future<bool> handleSubscription({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    return true;
  }
}

Ótimo! Agora você tem dois processadores de compras. Em seguida, crie o endpoint da API de verificação de compras.

Usar gerenciadores de compras

Abra bin/server.dart e crie um endpoint de API usando shelf_route:

bin/server.dart

import 'dart:convert';

import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/products.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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 faz o seguinte:

  1. Defina um endpoint POST que será chamado do app criado anteriormente.
  2. Decodifique o payload JSON e extraia as seguintes informações:
    1. userId: ID do usuário conectado
    2. source: loja usada, app_store ou google_play.
    3. productData: obtido do productDataMap criado anteriormente.
    4. token: contém os dados de verificação a serem enviados às lojas.
  3. Chame o método verifyPurchase para GooglePlayPurchaseHandler ou AppStorePurchaseHandler, dependendo da origem.
  4. Se a verificação for bem-sucedida, o método vai retornar um Response.ok ao cliente.
  5. Se a verificação falhar, o método vai retornar um Response.internalServerError ao cliente.

Depois de criar o endpoint da API, configure os dois manipuladores de compra. Para isso, carregue as chaves da conta de serviço obtidas na etapa anterior e configure o acesso aos diferentes serviços, incluindo a API Android Publisher e a API Firebase Firestore. Em seguida, crie os dois manipuladores de compra com as diferentes dependências:

bin/server.dart

import 'dart:convert';
import 'dart:io'; // new

import 'package:firebase_backend_dart/app_store_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/google_play_purchase_handler.dart'; // new
import 'package:firebase_backend_dart/helpers.dart';
import 'package:firebase_backend_dart/iap_repository.dart'; // new
import 'package:firebase_backend_dart/products.dart';
import 'package:firebase_backend_dart/purchase_handler.dart'; // new
import 'package:googleapis/androidpublisher/v3.dart' as ap; // new
import 'package:googleapis/firestore/v1.dart' as fs; // new
import 'package:googleapis_auth/auth_io.dart' as auth; // new
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.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 implementando o manipulador de compras do Google Play.

O Google já oferece pacotes Dart para interagir com as APIs necessárias para verificar compras. Você os inicializou no arquivo server.dart e agora os usa na classe GooglePlayPurchaseHandler.

Implemente o gerenciador para compras que não são de assinatura:

lib/google_play_purchase_handler.dart

  /// Handle non-subscription purchases (one time purchases).
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @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 don't 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;
  }

Você pode atualizar o manipulador 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 don't 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 dos IDs de pedidos, bem como 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;
}

Suas compras no Google Play agora serão verificadas e armazenadas no banco de dados.

Em seguida, acesse as compras da App Store para iOS.

Verificar compras no iOS: implementar o manipulador 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 manipuladores. Mescle-os para que 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 {

    // See next step
  }

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) {
      print('Successfully verified purchase');
      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;
    }
  }

Suas compras na App Store agora serão verificadas e armazenadas no banco de dados.

Executar o back-end

Neste ponto, você pode executar dart bin/server.dart para veicular o endpoint /verifypurchase.

$ dart bin/server.dart
Serving at http://0.0.0.0:8080

11. Acompanhe suas compras

A maneira recomendada de rastrear as compras dos usuários é no serviço de back-end. Isso acontece porque seu back-end pode responder a eventos da loja e, portanto, é menos propenso a ter informações desatualizadas devido ao armazenamento em cache, além de ser menos suscetível a adulterações.

Primeiro, configure o processamento de eventos da loja no back-end com o back-end do Dart que você está criando.

Processar eventos da loja no back-end

As lojas podem informar seu back-end sobre eventos de faturamento, como a renovação de assinaturas. Você pode processar esses eventos no back-end para manter as compras atualizadas no seu banco de dados. Nesta seção, configure isso para a Google Play Store e a Apple App Store.

Processar eventos do Google Play Faturamento

O Google Play fornece eventos de faturamento pelo que eles chamam de tópico do Cloud Pub/Sub. Essas são essencialmente filas de mensagens em que as mensagens podem ser publicadas e consumidas.

Como essa funcionalidade é específica do Google Play, inclua-a 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 de 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 _pullMessageFromPubSub a cada dez segundos. Você pode ajustar a duração de acordo com sua preferência.

Em seguida, crie o _pullMessageFromPubSub

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/$googleCloudProjectId/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/$googleCloudProjectId/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 pede novas mensagens. Em seguida, processa cada mensagem no método _processMessage.

Esse método decodifica as mensagens recebidas e obtém as informações atualizadas sobre cada compra, tanto assinaturas quanto não assinaturas, chamando o handleSubscription ou handleNonSubscription atual, 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

import 'package:googleapis/pubsub/v1.dart' as pubsub;      // Add this import

  final clientGooglePlay = await auth
      .clientViaServiceAccount(clientCredentialsGooglePlay, [
        ap.AndroidPublisherApi.androidpublisherScope,
        pubsub.PubsubApi.cloudPlatformScope,               // Add this line
      ]);

Em seguida, crie a instância PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Por fim, transmita para o construtor GooglePlayPurchaseHandler:

bin/server.dart

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,                                           // Add this line
    ),
    '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 isso.

Primeiro, crie um tópico do Pub/Sub:

  1. Defina o valor de googleCloudProjectId em constants.dart como o ID do seu projeto do Google Cloud.
  2. Acesse a página do Cloud Pub/Sub no console do Google Cloud.
  3. Verifique se você está no seu projeto do Firebase e clique em + Criar tópico. d5ebf6897a0a8bf5.png
  4. Dê ao novo tópico um nome idêntico ao valor definido para googlePlayPubsubBillingTopic em constants.dart. Neste caso, nomeie-o como play_billing. Se você escolher outra opção, atualize constants.dart. Crie o tópico. 20d690fc543c4212.png
  5. 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. ea03308190609fb.png
  6. Na barra lateral à direita, escolha Adicionar principal.
  7. Adicione google-play-developer-notifications@system.gserviceaccount.com e conceda a ele a função de Editor do Pub/Sub. 55631ec0549215bc.png
  8. Salve as mudanças de permissão.
  9. Copie o Nome do tópico que você acabou de criar.
  10. Abra o Play Console de novo e escolha seu app na lista Todos os apps.
  11. Role a tela para baixo e acesse Monetização > Configuração de monetização.
  12. Preencha o tópico completo e salve as mudanças. 7e5e875dc6ce5d54.png

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.

O motivo de este codelab se concentrar na segunda solução é que você precisaria 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 do servidor caso você tenha perdido um evento ou precise verificar novamente 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;                 // Add this member

  AppStorePurchaseHandler(
    this.iapRepository,
    this.appStoreServerAPI,                                  // And this parameter
  );

Modifique o construtor para adicionar um timer que vai chamar o método _pullStatus. Esse timer vai chamar o método _pullStatus a cada 10 segundos. Você pode 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

  /// Request the App Store for the latest subscription status.
  /// Updates all App Store subscriptions in the database.
  /// NOTE: This code only handles when a subscription expires as example.
  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:

  1. Recebe a lista de assinaturas ativas do Firestore usando o IapRepository.
  2. Para cada pedido, ele solicita o status da assinatura à API do servidor da App Store.
  3. Recebe a última transação da compra da assinatura.
  4. Verifica a data de validade.
  5. Atualiza o status da assinatura no Firestore. Se ela estiver expirada, será marcada como tal.

Por fim, adicione todo o código necessário para configurar o acesso à API do servidor da App Store:

bin/server.dart

import 'package:app_store_server_sdk/app_store_server_sdk.dart';  // Add this import
import 'package:firebase_backend_dart/constants.dart';            // And this one.


  // 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,                                     // Add this argument
    ),
  };

Configuração da App Store

Em seguida, configure a App Store:

  1. Faça login no App Store Connect e selecione Usuários e acesso.
  2. Acesse Integrações > Chaves > Compra no app.
  3. Toque no ícone de adição para adicionar um novo.
  4. Dê um nome a ela, como "Chave do codelab".
  5. Faça o download do arquivo .p8 que contém a chave.
  6. Copie para a pasta de recursos com o nome SubscriptionKey.p8.
  7. Copie o ID da chave recém-criada e defina-o como a constante appStoreKeyId no arquivo lib/constants.dart.
  8. Copie o ID do emissor no topo da lista de chaves e defina-o como a constante appStoreIssuerId no arquivo lib/constants.dart.

9540ea9ada3da151.png

Rastrear compras no dispositivo

A maneira mais segura de rastrear suas compras é no lado do servidor, porque o cliente é difícil de proteger. No entanto, você precisa ter uma maneira de enviar as informações de volta para o cliente para que o app possa agir com base nas informações de status da assinatura. Ao armazenar as compras no Firestore, é possível sincronizar 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 está expirado. 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((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, que é onde as assinaturas devem 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 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() sempre que as compras no Firestore mudam, o método purchasesUpdate() é sempre chamado quando os produtos comprados mudam.

lib/logic/dash_purchases.dart

import '../repo/iap_repo.dart';                              // Add this import

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  FirebaseNotifier firebaseNotifier;
  StoreState storeState = StoreState.loading;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<PurchasableProduct> products = [];
  IAPRepo iapRepo;                                           // Add this line

  bool get beautifiedDash => _beautifiedDashUpgrade;
  bool _beautifiedDashUpgrade = false;
  final iapConnection = IAPConnection.instance;

  // Add this.iapRepo as a parameter
  DashPurchases(this.counter, this.firebaseNotifier, this.iapRepo) {
    final purchaseUpdated = iapConnection.purchaseStream;
    _subscription = purchaseUpdated.listen(
      _onPurchaseUpdate,
      onDone: _updateStreamOnDone,
      onError: _updateStreamOnError,
    );
    iapRepo.addListener(purchasesUpdate);
    loadPurchases();
  }

  Future<void> loadPurchases() async {
    // Elided.
  }

  @override
  void dispose() {
    _subscription.cancel();
    iapRepo.removeListener(purchasesUpdate);                 // Add this line
    super.dispose();
  }

  void purchasesUpdate() {
    //TODO manage updates
  }

Em seguida, forneça o IAPRepo ao construtor em main.dart.. É possível 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>(),                         // Add this line
          ),
          lazy: false,
        ),

Em seguida, escreva o código da função purchaseUpdate(). Em dash_counter.dart,, os métodos applyPaidMultiplier e removePaidMultiplier definem o multiplicador como 10 ou 1, respectivamente. Assim, não é necessário verificar se a assinatura já foi aplicada. Quando o status da assinatura mudar, atualize também o status do produto comprável para mostrar na página de compra que ele já está ativo. Defina a propriedade _beautifiedDashUpgrade com base na compra ou não 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 seu jogo Dash Clicker.

12. Pronto!

Parabéns! Você concluiu o codelab. O código completo deste codelab está na pasta android_studio_folder.png complete.

Para saber mais, confira os outros codelabs do Flutter.