Como adicionar compras ao seu app do Flutter

1. Introdução

Última atualização:11/07/2023

Para adicionar compras a um app Flutter, é preciso 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 (fornecido para você) e verificar essas compras usando um back-end Dart com Firebase. O app fornecido, Dash Clicker, contém um jogo que usa o mascote Dash como moeda. Você adicionará as seguintes opções de compra:

  1. Uma opção de compra repetível de 2.000 traços ao mesmo tempo.
  2. Um upgrade único para transformar a Dash antigo em uma moderna Dash.
  3. Uma assinatura que dobra os cliques gerados automaticamente.

A primeira opção de compra dá ao usuário um benefício direto de 2.000 traços. Eles estão disponíveis diretamente para o usuário e podem ser comprados várias vezes. Isso é chamado de consumível porque é diretamente consumido e pode ser consumido várias vezes.

A segunda opção atualiza a Dash para uma Dash mais bonita. Ele só precisa ser comprado uma vez e fica disponível para sempre. Essa compra é chamada de não de consumo 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 pela assinatura, os benefícios também vão desaparecer.

O serviço de back-end (também fornecido para você) é executado como um app Dart, verifica se as compras foram feitas e as armazena usando o Firestore. O Firestore é usado para facilitar o processo, mas você pode usar qualquer tipo de serviço de back-end no seu app de produção.

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

O que você vai criar

  • Ampliar um app para oferecer suporte a compras e assinaturas de consumo.
  • Você também vai estender um app de back-end Dart para verificar e armazenar os itens comprados.

O que você vai aprender

  • Como configurar a App Store e a Play Store com produtos à venda.
  • Como se comunicar com as lojas para verificar compras e armazená-las no Firestore.
  • Como gerenciar compras no seu app.

O que é necessário

  • Android Studio 4.1 ou versão mais recente
  • Xcode 12 ou versão mais recente (para desenvolvimento no iOS)
  • SDK do Flutter (em inglês)

2. Configurar o ambiente de desenvolvimento

Para iniciar este codelab, faça o download do código e mude o identificador do pacote no iOS e o nome do pacote no Android.

Fazer o download do código

Para clonar o repositório do GitHub (link em inglês) pela linha de comando, use o seguinte comando:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

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 para este 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, então é muito fácil localizar os arquivos correspondentes:

cd flutter-codelabs/in_app_purchases/step_00

Se quiser avançar ou conferir a aparência de algo depois de uma etapa, procure o diretório com o nome da etapa do seu interesse. O código da última etapa está na pasta complete.

Configurar o projeto inicial

Abra o projeto inicial em step_00 no seu ambiente de desenvolvimento integrado favorito. Usamos o Android Studio para as capturas de tela, mas o Visual Studio Code também é uma ótima opção. Em qualquer um dos editores, verifique se os plug-ins mais recentes do Dart e do Flutter estão instalados.

Os apps que você 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 Android Play Store, é o ID do aplicativo. Esses identificadores geralmente são criados com uma notação reversa de nome de domínio. Por exemplo, ao criar um app de compra no app para o flutter.dev, usamos dev.flutter.inapppurchase. Pense em um identificador para seu app. Agora você vai definir isso nas configurações do projeto.

Primeiro, configure o identificador do pacote para iOS.

Com o projeto aberto no Android Studio, clique com o botão direito do mouse na pasta iOS, clique em Flutter e abra o módulo no app Xcode.

942772eb9a73bfaa.png

Na estrutura de pastas do Xcode, o projeto Runner está na parte superior, e os destinos Flutter, Runner e Products estão abaixo do projeto Runner. Clique duas vezes em Runner para editar as configurações do projeto e clique em Assinatura e Recursos. Digite o identificador do pacote que você acabou de escolher no campo Equipe para definir sua equipe.

812f919d965c649a.jpeg

Você já pode fechar o Xcode e voltar ao Android Studio para concluir a configuração do Android. Para fazer isso, abra o arquivo build.gradle em android/app, e mude o applicationId (na linha 37 na captura de tela abaixo) para o ID do aplicativo, o mesmo que o identificador do pacote iOS. Os IDs das lojas para iOS e Android não precisam ser idênticos. No entanto, mantê-los idênticos é menos propenso a erros. Por isso, neste codelab também vamos usar identificadores idênticos.

5c4733ac560ae8c2.png

3. Instalar o plug-in

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

Adicionar dependência no pubspec

Adicione in_app_purchase ao pubspec adicionando in_app_purchase às dependências no seu pubspec:

$ cd app
$ flutter pub add in_app_purchase

pubspec.yaml

dependencies:
  ..
  cloud_firestore: ^4.0.3
  firebase_auth: ^4.2.2
  firebase_core: ^2.5.0
  google_sign_in: ^6.0.1
  http: ^0.13.4
  in_app_purchase: ^3.0.1
  intl: ^0.18.0
  provider: ^6.0.2
  ..

Clique em pub get para fazer o download do pacote ou execute flutter pub get na linha de comando.

4. Configurar a App Store

Para configurar compras no aplicativo e testá-las no iOS, você precisa criar um novo aplicativo na App Store e criar produtos que podem ser comprados. Não é necessário publicar nada nem enviar o app à Apple para revisão. Você precisa de uma conta de desenvolvedor para fazer isso. Caso ainda não tenha, inscreva-se no Programa para 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 contas.

6e373780e5e24a6f.png

Aqui você verá contratos aqui para apps sem custo financeiro e pagos. O status de apps sem custo financeiro deve ser ativo, e o status de apps pagos deve ser novo. Lembre-se de ler os termos, de aceitá-los e inserir todas as informações necessárias.

74c73197472c9aec.png

Quando tudo estiver definido corretamente, o status dos apps pagos será exibido como ativo. Isso é muito importante porque não é possível testar compras no app sem um contrato ativo.

4a100bbb8cafdbbf.jpeg

Registrar o ID do app

Crie um novo identificador no portal para desenvolvedores da Apple.

55d7e592d9a3fc7b.png

Escolher IDs de apps

13f125598b72ca77.png

Selecione um app

41ac4c13404e2526.png

Forneça algumas descrições e defina o ID do pacote para corresponder ao ID do pacote com o mesmo valor definido anteriormente no XCode.

9d2c940ad80deeef.png

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

Como criar um novo app

Crie um novo 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 de sandbox. Esse usuário de teste não deve estar conectado ao iTunes. Ele é usado somente para testar compras no app. Não é possível usar um endereço de e-mail que já é usado em uma conta da Apple. Em Usuários e acesso, acesse Testadores em Sandbox para criar uma nova conta do sandbox ou gerenciar os IDs Apple do sandbox existentes.

3ca2b26d4e391a4c.jpeg

Agora, você pode configurar seu usuário do sandbox em seu iPhone acessando Ajustes > App Store > conta do sandbox.

c7dadad2c1d448fa.jpeg 5363f87efcddaa4.jpeg

Como configurar compras no app

Agora você vai configurar os três itens que podem ser comprados:

  • dash_consumable_2k: uma compra de consumo que pode ser comprada várias vezes, concedendo ao usuário 2.000 traços (a moeda no app) por compra.
  • dash_upgrade_3d: um "upgrade" não consumível. que só pode ser comprada uma vez e dá ao usuário uma Dash cosmeticamente diferente para clicar.
  • dash_subscription_doubler: uma assinatura que concede ao usuário o dobro de traços por clique durante o período da assinatura.

d156b2f5bac43ca8.png

Acesse Compras no app > Gerenciar.

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 na App Store Connect. Basta defini-lo como dash consumable 2k e adicionar as traduções para a compra. Chame a compra de Spring is in the air com 2000 dashes fly out como descrição.

ec1701834fd8527.png

  1. Configure dash_upgrade_3d como não de consumo.

Use dash_upgrade_3d como o ID do produto. Defina o nome de referência como dash upgrade 3d e adicione as localizações da compra. Chame a compra de 3D Dash com Brings your dash back to the future como descrição.

6765d4b711764c30.png

  1. Configure dash_subscription_doubler como uma assinatura com renovação automática.

O fluxo das assinaturas é um pouco diferente. Primeiro, você precisa definir o nome da referência e o ID do produto:

6d29e08dae26a0c4.png

Em seguida, você precisa criar um grupo de assinaturas. Quando várias assinaturas fazem parte de um mesmo grupo, um usuário só pode se inscrever em uma delas ao mesmo tempo, mas pode fazer upgrade ou downgrade entre essas assinaturas. Chame este grupo de subscriptions.

5bd0da17a85ac076.png

Em seguida, insira a duração da assinatura e as localizações. Nomeie esta assinatura como Jet Engine com a descrição Doubles your clicks. Clique em Salvar.

bd1b1d82eeee4cb3.png

Depois de clicar no botão Salvar, adicione o preço da assinatura. Escolha o preço que quiser.

d0bf39680ef0aa2e.png

Agora, você verá as três compras na lista:

99d5c4b446e8fecf.png

5. Configurar a Play Store

Assim como na App Store, também é necessário ter uma conta de desenvolvedor para a Play Store. Registre uma conta caso ainda não tenha uma.

Criar um novo app

Crie um novo 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. e pode ser mudado depois.
  4. Especifique que o aplicativo é um jogo. Isso pode ser alterado mais tarde.
  5. Especifique se o aplicativo é sem custo financeiro ou pago.
  6. Adicione um endereço de e-mail para que os usuários da Play Store possam entrar em contato com você sobre o app.
  7. Preencha as diretrizes de conteúdo e as declarações das leis de exportação dos EUA.
  8. Selecione Criar app.

Após a criação do app, 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 as compras no app, é preciso ter pelo menos um build enviado ao Google Play.

Para isso, seu build de lançamento precisa ser assinado com algo diferente das chaves de depuração.

Criar um keystore

Se você já tiver um keystore, pule para a próxima etapa. Caso contrário, crie um executando o seguinte na linha de comando.

No Mac/Linux, use o seguinte comando:

keytool -genkey -v -keystore ~/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key

No Windows, use o seguinte comando:

keytool -genkey -v -keystore c:\Users\USER_NAME\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias key

Esse comando armazena o arquivo key.jks no seu diretório inicial. Se você quiser armazenar o arquivo em outro lugar, mude o argumento transmitido para o parâmetro -keystore. Mantenha a

keystore

arquivo privado; não faça check-in no controle de código-fonte público.

Referenciar o 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

Para configurar a assinatura do app, edite o arquivo <your app dir>/android/app/build.gradle.

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

   def keystoreProperties = new Properties()
   def keystorePropertiesFile = rootProject.file('key.properties')
   if (keystorePropertiesFile.exists()) {
       keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
   }

   android {
         // omitted
   }

Carregue o arquivo key.properties no objeto keystoreProperties.

Adicione o código abaixo antes do bloco buildTypes:

   buildTypes {
       release {
           // TODO: Add your own signing config for the release build.
           // Signing with the debug keys for now,
           // so `flutter run --release` works.
           signingConfig signingConfigs.debug
       }
   }

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

   signingConfigs {
       release {
           keyAlias keystoreProperties['keyAlias']
           keyPassword keystoreProperties['keyPassword']
           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
           storePassword keystoreProperties['storePassword']
       }
   }
   buildTypes {
       release {
           signingConfig signingConfigs.release
       }
   }

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

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

Fazer upload do seu primeiro build

Depois que o app estiver configurado para assinatura, você poderá criá-lo executando:

flutter build appbundle

Esse comando gera um build de lançamento por padrão. A saída pode ser encontrada em <your app dir>/build/app/outputs/bundle/release/.

No painel do Google Play Console, acesse Versão > Teste > Faça testes fechados e crie uma nova versão de teste fechado.

Neste codelab, você usará a assinatura do Google no app. Portanto, pressione Continuar em Assinatura de apps do Google Play para ativar o recurso.

ba98446d9c5c40e0.png

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

Clique em Salvar e em Avaliar versão.

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

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 Versão > Teste > Testes internos e clique na guia Testadores.

a0d0394e85128f84.png

Crie uma nova lista de e-mails clicando 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 para testar 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 compras no app.
  4. Defina a Resposta de licença como RESPOND_NORMALLY.
  5. Clique em Salvar alterações.

a1a0f9d3e55ea8da.png

Como configurar 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 de consumo que pode ser comprada várias vezes, concedendo ao usuário 2.000 traços (a moeda no app) por compra.
  • dash_upgrade_3d: um "upgrade" não consumível. compra que só pode ser comprada uma vez, que dá ao usuário uma Dash cosmeticamente diferente para clicar.
  • dash_subscription_doubler: uma assinatura que concede ao usuário o dobro de traços por clique durante o período da assinatura.

Primeiro, adicione os itens de consumo e não de consumo.

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

Em seguida, adicione a assinatura:

  1. Acesse o Google Play Console e selecione seu app.
  2. Acesse Gerar receita > 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ê quer usar.
  5. Clique em Salvar.

Suas compras agora devem ser configuradas no Play Console.

6. Configurar o Firebase

Neste codelab, você vai usar um serviço de back-end para verificar e rastrear compras.

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

  • As transações podem ser verificadas com segurança.
  • É possível reagir a eventos de faturamento das app stores.
  • É possível acompanhar as compras em um banco de dados.
  • Os usuários não poderão enganar seu aplicativo para fornecer recursos premium retrocedendo o relógio do sistema.

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

A criação do back-end é considerada fora do escopo deste codelab, portanto, o código inicial já inclui um projeto do Firebase que gerencia compras básicas para você começar.

Os plug-ins do Firebase também estão incluídos no app inicial.

Agora você precisa criar seu próprio projeto do Firebase, configurar o app e o back-end para o Firebase e, por fim, implantar o back-end.

Criar um projeto do Firebase

Acesse o Console do Firebase e crie um novo projeto do Firebase. Para este exemplo, chame o projeto Dash Clicker.

No aplicativo de back-end, você vincula as compras a um usuário específico, portanto, precisa de autenticação. Para isso, use o módulo de autenticação do Firebase com o Login do Google.

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

7babb48832fbef29.png

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

e20553e0de5ac331.png

Defina as regras do Cloud Firestore desta maneira:

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 conforme explicado na página de configuração.

Ao executar a configuração do flutterfire, selecione o projeto que você criou na etapa anterior.

$ flutterfire configure

i Found 5 Firebase projects.                                                                                                  
? Select a Firebase project to configure your Flutter application with ›                                                      
❯ in-app-purchases-1234 (in-app-purchases-1234)                                                                         
  other-flutter-codelab-1 (other-flutter-codelab-1)                                                                           
  other-flutter-codelab-2 (other-flutter-codelab-2)                                                                      
  other-flutter-codelab-3 (other-flutter-codelab-3)                                                                           
  other-flutter-codelab-4 (other-flutter-codelab-4)                                                                                                                                                               
  <create a new project>  

Em seguida, ative iOS e Android selecionando as duas plataformas.

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

Quando solicitado sobre a substituição de 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 para baixo até Seus apps e selecione o app dashclicker (Android).

b22d46a759c0c834.png

Para permitir o Login do Google no modo de depuração, é necessário fornecer a impressão digital do hash SHA-1 do seu certificado de depuração.

Receber o hash do certificado de assinatura de depuração

Na raiz do seu projeto de app do 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 será exibida. Como você está procurando o hash do certificado de depuração, procure o certificado com as propriedades Variant e Config definidas como debug. É provável que o keystore esteja na pasta inicial em .android/debug.keystore.

> Task :app:signingReport
Variant: debug
Config: debug
Store: /<USER_HOME_FOLDER>/.android/debug.keystore
Alias: AndroidDebugKey
MD5: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA1: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
SHA-256: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX
Valid until: Tuesday, January 19, 2038

Copie o hash SHA-1 e preencha o último campo na caixa de diálogo modal de envio do app.

Configurar o Firebase para iOS: outras etapas

Abra o ios/Runnder.xcworkspace com Xcode. Ou com o ambiente de desenvolvimento integrado de sua preferência.

No VSCode, clique com o botão direito do mouse na pasta ios/ e depois em open in xcode.

No Android Studio, clique com o botão direito do mouse na pasta ios/, depois clique em flutter e depois na 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 os documentos do pacote google_sign_in para mais informações. Neste caso, os arquivos são ios/Runner/Info-Debug.plist e ios/Runner/Info-Release.plist.

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

  1. Consiga o valor de REVERSED_CLIENT_ID do arquivo GoogleService-Info.plist, sem o elemento <string>..</string> ao redor dele.
  2. Substitua o valor nos arquivos ios/Runner/Info-Debug.plist e ios/Runner/Info-Release.plist pela 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>

Você concluiu a configuração do Firebase.

7. Ouvir atualizações de compras

Nesta parte do codelab, você vai preparar o app para comprar os produtos. Esse processo inclui detectar atualizações de compras e erros após a inicialização do app.

Ouvir atualizações de compras

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

Por padrão, o objeto de um provedor é definido quando ele é solicitado pela primeira vez. Esse objeto detecta atualizações de compra diretamente quando o app é iniciado. Portanto, desative o carregamento lento nesse objeto com lazy: false:

lib/main.dart

ChangeNotifierProvider<DashPurchases>(
  create: (context) => DashPurchases(
    context.read<DashCounter>(),
  ),
  lazy: false,
),

Você também precisa de uma instância do InAppPurchaseConnection. No entanto, para manter o app testável, é necessário simular a conexão de alguma forma. 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!;
  }
}

Faça uma pequena atualização no teste se quiser que ele continue funcionando. Consulte widget_test.dart no GitHub para ver o código completo de TestIAPConnection.

test/widget_test.dart

void main() {
  testWidgets('App starts', (WidgetTester tester) async {
    IAPConnection.instance = TestIAPConnection();
    await tester.pumpWidget(const MyApp());
    expect(find.text('Tim Sneath'), findsOneWidget);
  });
}

No lib/logic/dash_purchases.dart, acesse o código de DashPurchases ChangeNotifier. No momento, só é possível adicionar um DashCounter aos traços 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 será parecido com este:

lib/logic/dash_purchases.dart

import 'package:in_app_purchase/in_app_purchase.dart';

class DashPurchases extends ChangeNotifier {
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

  DashPurchases(this.counter);
}

A palavra-chave late é adicionada ao _subscription porque o _subscription é inicializado no construtor. Por padrão, esse projeto está configurado para não ser anulável (NNBD, na sigla em inglês), o que significa que as propriedades que não são declaradas como anuláveis precisam ter um valor não nulo. O qualificador late permite atrasar a definição desse valor.

No construtor, acesse o purchaseUpdatedStream e comece a detectar o stream. No método dispose(), cancele a assinatura de stream.

lib/logic/dash_purchases.dart

class DashPurchases extends ChangeNotifier {
  DashCounter counter;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  final iapConnection = IAPConnection.instance;

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

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  Future<void> buy(PurchasableProduct product) async {
    // omitted
  }

  void _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) {
    // Handle purchases here
  }

  void _updateStreamOnDone() {
    _subscription.cancel();
  }

  void _updateStreamOnError(dynamic error) {
    //Handle error here
  }
}

Agora, o app recebe as atualizações da compra, então você vai fazer uma compra na próxima seção.

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 à venda. Esses produtos são carregados nas lojas, mostrados em uma lista e comprados ao tocar no produto.

Adapt PurchasableProduct (em inglês)

PurchasableProduct mostra um produto simulado. Atualize a classe para mostrar o conteúdo real substituindo a classe PurchasableProduct no 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 as substitua por uma lista vazia, List<PurchasableProduct> products = [];.

Carregar compras disponíveis

Para permitir que o usuário faça uma compra, carregue as compras da loja. Primeiro, verifique se a loja está disponível. Quando a loja não estiver disponível, definir storeState como notAvailable exibirá 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 do Firebase, serão mostrados storeKeyConsumable, storeKeySubscription, e storeKeyUpgrade. Quando uma compra esperada não estiver disponível, imprima essas informações no console. envie essas informações para o serviço de back-end.

O método await iapConnection.queryProductDetails(ids) retorna os IDs que não foram encontrados e os produtos que podem ser comprados. Use o productDetails da resposta para atualizar a interface e defina 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);
    for (var element in response.notFoundIDs) {
      debugPrint('Purchase $element not found');
    }
    products = response.productDetails.map((e) => PurchasableProduct(e)).toList();
    storeState = StoreState.available;
    notifyListeners();
  }

Chame a função loadPurchases() no construtor:

lib/logic/dash_purchases.dart

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

Por fim, mude o valor do campo storeState de StoreState.available para StoreState.loading:.

lib/logic/dash_purchases.dart

StoreState storeState = StoreState.loading;

Mostre os produtos que podem ser comprados

Considere o arquivo purchase_page.dart. O widget PurchasePage mostra _PurchasesLoading, _PurchaseList, ou _PurchasesNotAvailable,, dependendo do StoreState. O widget também mostra as compras anteriores do usuário, que serão usadas na próxima etapa.

O widget _PurchaseList mostra a lista de produtos compráveis e envia uma solicitação de compra para o 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(),
    );
  }
}

Você conseguirá ver os produtos disponíveis nas lojas para Android e iOS se eles estiverem configurados corretamente. Pode levar algum tempo até que as compras fiquem disponíveis quando inseridas nos respectivos consoles.

ca1a9f97c21e552d.png

Volte para dash_purchases.dart e implemente a função para comprar um produto. Você só precisa separar os produtos de consumo dos não consumíveis. O upgrade e os produtos por assinatura não são de consumo.

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);
        break;
      case storeKeySubscription:
      case storeKeyUpgrade:
        await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
        break;
      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 da 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 será tratada 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();
          break;
        case storeKeyConsumable:
          counter.addBoughtDashes(2000);
          break;
        case storeKeyUpgrade:
          _beautifiedDashUpgrade = true;
          break;
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

9. configurar o back-end

Antes de prosseguir com o rastreamento e a verificação de 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 foram incluídas no código inicial. É uma boa ideia revisar 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 em sua máquina, você não precisa implantá-lo para usá-lo. No entanto, você precisa conseguir se conectar do seu dispositivo de desenvolvimento (Android ou iPhone) à máquina em que o servidor será executado. Para isso, eles precisam estar na mesma rede, e você precisa saber o endereço IP da sua máquina.

Tente executar o servidor usando o seguinte comando:

$ dart ./bin/server.dart

Serving at http://0.0.0.0:8080

O back-end do Dart usa shelf e shelf_router para exibir endpoints de API. Por padrão, o servidor não fornece rotas. Mais tarde, você vai criar uma rota para lidar com o processo de 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. Gere uma para abrir as configurações do projeto do Firebase, acesse a seção Contas de serviço e selecione Gerar nova chave privada.

27590fc77ae94ad4.png

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

Configurar o acesso ao Google Play

Para acessar a Play Store e verificar compras, você precisa gerar uma conta de serviço com essas permissões e fazer o download das credenciais JSON para ela.

  1. Acesse o Google Play Console e comece pela página Todos os apps.
  2. Acesse Configuração > acesso à API. 317fdfb54921f50e.png Caso o Google Play Console solicite a criação ou a vinculação a um projeto existente, faça isso primeiro e depois volte a esta página.
  3. Encontre a seção em que é possível definir contas de serviço e clique em Criar nova conta de serviço.1e70d3f8d794bebb.png
  4. Clique no link Google Cloud Platform na caixa de diálogo que aparece. 7c9536336dd9e9b4.png
  5. Selecione o projeto. Se ela não aparecer, verifique se você fez login na Conta do Google correta na lista suspensa Conta no canto superior direito. 3fb3a25bad803063.png
  6. Depois de selecionar o projeto, clique em + Criar conta de serviço na barra de menus superior. 62fe4c3f8644acd8.png
  7. Forneça um nome para a conta de serviço e, se quiser, insira uma descrição para que você se lembre da finalidade dela e passe para a próxima etapa. 8a92d5d6a3dff48c.png
  8. Atribua o papel de Editor à conta de serviço. 6052b7753667ed1a.png
  9. Conclua o assistente, volte para a página Acesso à API no console do desenvolvedor e clique em Atualizar contas de serviço. Você verá sua conta recém-criada na lista. 5895a7db8b4c7659.png
  10. Clique em Conceder acesso para a nova conta de serviço.
  11. Role a página seguinte para baixo até o bloco Dados financeiros. Selecione Ver dados financeiros, pedidos e respostas à pesquisa de cancelamento e Gerenciar pedidos e assinaturas. 75b22d0201cf67e.png
  12. Clique em Convidar usuário. 70ea0b1288c62a59.png
  13. Agora que a conta está configurada, você só precisa gerar algumas credenciais. No console do Cloud, encontre sua conta de serviço na lista de contas de serviço, clique nos três pontos verticais e escolha Gerenciar chaves. 853ee186b0e9954e.png
  14. Crie uma nova chave JSON e faça o download dela. 2a33a55803f5299c.png cb4bf48ebac0364e.png
  15. Renomeie o arquivo salvo como service-account-google-play.json, e mova-o para o diretório assets/.

Mais uma coisa que precisamos fazer é abrir lib/constants.dart, e substituir o valor de androidPackageId pelo ID do pacote que você escolheu para seu app Android.

Configurar o acesso à Apple App Store

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

  1. Abra o App Store Connect.
  2. Acesse Meus apps e selecione seu app.
  3. Na navegação da barra lateral, vá até Compras no app > Gerenciar.
  4. No canto superior direito da lista, clique em Chave secreta compartilhada específica do app.
  5. Gere e copie um novo secret.
  6. Abra lib/constants.dart, e substitua o valor de appStoreSharedSecret pela chave secreta compartilhada que você acabou de gerar.

d8b8042470aaeff.png

b72f4565750e2f40.png

Arquivo de configuração de constantes

Antes de continuar, verifique se as seguintes constantes estão configuradas no arquivo lib/constants.dart:

  • androidPackageId: ID do pacote usado no Android. Por exemplo: com.example.dashclicker
  • appStoreSharedSecret: senha secreta para acessar o App Store Connect e realizar a verificação de compras.
  • bundleId: ID do pacote usado no iOS. Por exemplo: com.example.dashclicker

Você pode ignorar as outras constantes por enquanto.

10. Verificar compras

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

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

Esse token é enviado pelo aplicativo para seu serviço de back-end, que, por sua vez, verifica a compra com os servidores da respectiva loja usando o token fornecido.

O serviço de back-end pode, então, optar por armazenar a compra e responder ao aplicativo se a compra foi válida ou não.

Ao fazer com que o serviço de back-end faça a validação com as lojas em vez do aplicativo em execução no dispositivo do seu usuário, você pode impedir que o usuário tenha acesso a recursos premium, por exemplo, retrocedendo o relógio do sistema.

Configurar o lado do Flutter

Configurar a autenticação

Como você vai enviar as compras para o serviço de back-end, é importante garantir que o usuário seja autenticado durante a compra. A maior parte da lógica de autenticação já foi adicionada ao projeto inicial. Você só precisa garantir que o PurchasePage mostre o botão de login quando o usuário ainda não tiver feito login. Adicione o código abaixo ao início do método de build do PurchasePage:

lib/pages/purchase_page.dart

import '../logic/firebase_notifier.dart';
import '../model/firebase_state.dart';
import 'login_page.dart';

class PurchasePage extends StatelessWidget {  
  const PurchasePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var firebaseNotifier = context.watch<FirebaseNotifier>();
    if (firebaseNotifier.state == FirebaseState.loading) {
      return _PurchasesLoading();
    } else if (firebaseNotifier.state == FirebaseState.notAvailable) {
      return _PurchasesNotAvailable();
    }

    if (!firebaseNotifier.loggedIn) {
      return const LoginPage();
    }
    // omitted

Endpoint de verificação de chamada no app

No app, crie a função _verifyPurchase(PurchaseDetails purchaseDetails) que chama o endpoint /verifypurchase no back-end do Dart usando uma chamada de postagem HTTP.

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 que indica se a compra foi verificada.

Nas constantes do app, configure o IP do servidor como o endereço IP da máquina local.

lib/logic/dash_purchases.dart

  FirebaseNotifier firebaseNotifier;

  DashPurchases(this.counter, this.firebaseNotifier) {
    // omitted
  }

Adicione o firebaseNotifier com a criação de DashPurchases em main.dart:.

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
          ),
          lazy: false,
        ),

Adicione um getter ao usuário no FirebaseNotifier para transmitir o ID do usuário à função de verificação de compra.

lib/logic/firebase_notifier.dart

  User? get user => FirebaseAuth.instance.currentUser;

Adicione a função _verifyPurchase à classe DashPurchases. Essa função async retorna um booleano indicando se a compra foi validada.

lib/logic/dash_purchases.dart

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
    final url = Uri.parse('http://$serverIp:8080/verifypurchase');
    const headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
    };
    final response = await http.post(
      url,
      body: jsonEncode({
        'source': purchaseDetails.verificationData.source,
        'productId': purchaseDetails.productID,
        'verificationData':
            purchaseDetails.verificationData.serverVerificationData,
        'userId': firebaseNotifier.user?.uid,
      }),
      headers: headers,
    );
    if (response.statusCode == 200) {
      print('Successfully verified purchase');
      return true;
    } else {
      print('failed request: ${response.statusCode} - ${response.body}');
      return false;
    }
  }

Chame a função _verifyPurchase em _handlePurchase antes de aplicar a compra. Aplique a compra somente após ela ser verificada. Em um app de produção, é possível especificar isso também para, por exemplo, aplicar uma assinatura de teste quando a loja estiver temporariamente indisponível. No entanto, neste exemplo, simplifique e só aplique a compra quando ela for verificada.

lib/logic/dash_purchases.dart

  Future<void> _onPurchaseUpdate(
      List<PurchaseDetails> purchaseDetailsList) async {
    for (var purchaseDetails in purchaseDetailsList) {
      await _handlePurchase(purchaseDetails);
    }
    notifyListeners();
  }

  Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
    if (purchaseDetails.status == PurchaseStatus.purchased) {
      // Send to server
      var validPurchase = await _verifyPurchase(purchaseDetails);

      if (validPurchase) {
        // Apply changes locally
        switch (purchaseDetails.productID) {
          case storeKeySubscription:
            counter.applyPaidMultiplier();
            break;
          case storeKeyConsumable:
            counter.addBoughtDashes(1000);
            break;
        }
      }
    }

    if (purchaseDetails.pendingCompletePurchase) {
      await iapConnection.completePurchase(purchaseDetails);
    }
  }

Agora tudo está pronto no app para validar as compras.

Configurar o serviço de back-end

Em seguida, configure a função do Cloud para verificar compras no back-end.

Criar gerenciadores de compras

Como o fluxo de verificação para as duas lojas é quase idêntico, configure uma classe PurchaseHandler abstrata 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

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 notar, cada método requer três parâmetros:

  • userId: O ID do usuário conectado para que você possa vincular as 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 gerenciadores de compra, adicione um método verifyPurchase() que possa ser usado para assinaturas e não assinaturas:

lib/purchase_handler.dart

  /// Verify if purchase is valid and update the database
  Future<bool> verifyPurchase({
    required String userId,
    required ProductData productData,
    required String token,
  }) async {
    switch (productData.type) {
      case ProductType.subscription:
        return handleSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
      case ProductType.nonSubscription:
        return handleNonSubscription(
          userId: userId,
          productData: productData,
          token: token,
        );
    }
  }

Agora, você pode apenas 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 compráveis, que incluem o ID do produto (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 do gerenciador. você os comunicará mais tarde.

Como você pode ter notado, o construtor usa uma instância do IapRepository. O gerenciador de compras usa essa instância para armazenar informações sobre compras no Firestore mais tarde. Para se comunicar com o Google Play, use o AndroidPublisherApi fornecido.

Em seguida, faça o mesmo para o gerenciador da app store. Crie lib/app_store_purchase_handler.dart e adicione uma classe que estenda o PurchaseHandler novamente:

lib/app_store_purchase_handler.dart

import 'dart:async';

import 'package:app_store_server_sdk/app_store_server_sdk.dart';

import 'constants.dart';
import 'iap_repository.dart';
import 'products.dart';
import 'purchase_handler.dart';

class AppStorePurchaseHandler extends PurchaseHandler {
  final IapRepository iapRepository;

  AppStorePurchaseHandler(
    this.iapRepository,
  );

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

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

Ótimo! Agora você tem dois gerenciadores de compras. Em seguida, vamos criar 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

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);
}

({
  String userId,
  String source,
  ProductData productData,
  String token,
}) getPurchaseData(dynamic payload) {
  if (payload
      case {
        'userId': String userId,
        'source': String source,
        'productId': String productId,
        'verificationData': String token,
      }) {
    return (
      userId: userId,
      source: source,
      productData: productDataMap[productId]!,
      token: token,
    );
  } else {
    throw const FormatException('Unexpected JSON');
  }
}

O código acima faz o seguinte:

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

Depois de criar o endpoint da API, você precisa configurar os dois gerenciadores de compras. Para isso, carregue as chaves da conta de serviço que você recebeu 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 gerenciadores de compra com as diferentes dependências:

bin/server.dart

Future<Map<String, PurchaseHandler>> _createPurchaseHandlers() async {
  // Configure Android Publisher API access
  final serviceAccountGooglePlay =
      File('assets/service-account-google-play.json').readAsStringSync();
  final clientCredentialsGooglePlay =
      auth.ServiceAccountCredentials.fromJson(serviceAccountGooglePlay);
  final clientGooglePlay =
      await auth.clientViaServiceAccount(clientCredentialsGooglePlay, [
    ap.AndroidPublisherApi.androidpublisherScope,
  ]);
  final androidPublisher = ap.AndroidPublisherApi(clientGooglePlay);

  // Configure Firestore API access
  final serviceAccountFirebase =
      File('assets/service-account-firebase.json').readAsStringSync();
  final clientCredentialsFirebase =
      auth.ServiceAccountCredentials.fromJson(serviceAccountFirebase);
  final clientFirebase =
      await auth.clientViaServiceAccount(clientCredentialsFirebase, [
    fs.FirestoreApi.cloudPlatformScope,
  ]);
  final firestoreApi = fs.FirestoreApi(clientFirebase);
  final dynamic json = jsonDecode(serviceAccountFirebase);
  final projectId = json['project_id'] as String;
  final iapRepository = IapRepository(firestoreApi, projectId);

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };
}

Verificar compras no Android: implementar o gerenciador de compras

Em seguida, continue implementando o gerenciador de compras do Google Play.

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

Implemente o gerenciador para compras sem tipo de assinatura:

lib/google_play_purchase_handler.dart

  @override
  Future<bool> handleNonSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleNonSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.products.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Purchases response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = response.orderId!;

      final purchaseData = NonSubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.purchaseTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _nonSubscriptionStatusFrom(response.purchaseState),
        userId: userId,
        iapSource: IAPSource.googleplay,
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle NonSubscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle NonSubscription: $e\n');
    }
    return false;
  }

É possível atualizar o gerenciador de compras de assinatura de maneira semelhante:

lib/google_play_purchase_handler.dart

  /// Handle subscription purchases.
  ///
  /// Retrieves the purchase status from Google Play and updates
  /// the Firestore Database accordingly.
  @override
  Future<bool> handleSubscription({
    required String? userId,
    required ProductData productData,
    required String token,
  }) async {
    print(
      'GooglePlayPurchaseHandler.handleSubscription'
      '($userId, ${productData.productId}, ${token.substring(0, 5)}...)',
    );

    try {
      // Verify purchase with Google
      final response = await androidPublisher.purchases.subscriptions.get(
        androidPackageId,
        productData.productId,
        token,
      );

      print('Subscription response: ${response.toJson()}');

      // Make sure an order id exists
      if (response.orderId == null) {
        print('Could not handle purchase without order id');
        return false;
      }
      final orderId = extractOrderId(response.orderId!);

      final purchaseData = SubscriptionPurchase(
        purchaseDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.startTimeMillis ?? '0'),
        ),
        orderId: orderId,
        productId: productData.productId,
        status: _subscriptionStatusFrom(response.paymentState),
        userId: userId,
        iapSource: IAPSource.googleplay,
        expiryDate: DateTime.fromMillisecondsSinceEpoch(
          int.parse(response.expiryTimeMillis ?? '0'),
        ),
      );

      // Update the database
      if (userId != null) {
        // If we know the userId,
        // update the existing purchase or create it if it does not exist.
        await iapRepository.createOrUpdatePurchase(purchaseData);
      } else {
        // If we do not know the user id, a previous entry must already
        // exist, and thus we'll only update it.
        await iapRepository.updatePurchase(purchaseData);
      }
      return true;
    } on ap.DetailedApiRequestError catch (e) {
      print(
        'Error on handle Subscription: $e\n'
        'JSON: ${e.jsonResponse}',
      );
    } catch (e) {
      print('Error on handle Subscription: $e\n');
    }
    return false;
  }
}

Adicione o método a seguir para facilitar a análise de códigos de pedido, bem como dois métodos para analisar o status de compra.

lib/google_play_purchase_handler.dart

/// 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;
}

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,
  };
}

Suas compras do Google Play agora devem ser verificadas e armazenadas no banco de dados.

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

Verificar compras no iOS: implementar o gerenciador de compras

Para verificar compras com a App Store, existe um pacote Dart de terceiros chamado app_store_server_sdk que facilita o processo.

Comece criando a instância ITunesApi. Use a configuração de 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 ambos os 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 {
   //..
  }

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 devem ser verificadas e armazenadas no banco de dados!

Executar o back-end

Neste ponto, é possível executar dart bin/server.dart para exibir o endpoint /verifypurchase.

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

11. Acompanhe suas compras

A maneira recomendada de acompanhar compras fica no serviço de back-end. Isso ocorre porque seu back-end pode responder a eventos do armazenamento e, portanto, é menos propenso a encontrar 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 de armazenamento no back-end

As lojas podem informar ao back-end sobre quaisquer eventos de faturamento que ocorrerem, como quando assinaturas forem renovadas. É possível processar esses eventos no back-end para manter as compras atualizadas no banco de dados. Nesta seção, faça a configuração para a Google Play Store e a App Store da Apple.

Processar eventos de faturamento do Google Play

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

Como essa é uma funcionalidade específica do Google Play, ela precisa ser incluída 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 _pullMessageFromSubSub a cada 10 segundos. Ajuste a Duração como preferir.

Depois, crie a _pullMessageFromSubSub

lib/google_play_purchase_handler.dart

  /// Process messages from Google Play
  /// Called every 10 seconds
  Future<void> _pullMessageFromPubSub() async {
    print('Polling Google Play messages');
    final request = pubsub.PullRequest(
      maxMessages: 1000,
    );
    final topicName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    final pullResponse = await pubsubApi.projects.subscriptions.pull(
      request,
      topicName,
    );
    final messages = pullResponse.receivedMessages ?? [];
    for (final message in messages) {
      final data64 = message.message?.data;
      if (data64 != null) {
        await _processMessage(data64, message.ackId);
      }
    }
  }

  Future<void> _processMessage(String data64, String? ackId) async {
    final dataRaw = utf8.decode(base64Decode(data64));
    print('Received data: $dataRaw');
    final dynamic data = jsonDecode(dataRaw);
    if (data['testNotification'] != null) {
      print('Skip test messages');
      if (ackId != null) {
        await _ackMessage(ackId);
      }
      return;
    }
    final dynamic subscriptionNotification = data['subscriptionNotification'];
    final dynamic oneTimeProductNotification =
        data['oneTimeProductNotification'];
    if (subscriptionNotification != null) {
      print('Processing Subscription');
      final subscriptionId =
          subscriptionNotification['subscriptionId'] as String;
      final purchaseToken = subscriptionNotification['purchaseToken'] as String;
      final productData = productDataMap[subscriptionId]!;
      final result = await handleSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else if (oneTimeProductNotification != null) {
      print('Processing NonSubscription');
      final sku = oneTimeProductNotification['sku'] as String;
      final purchaseToken =
          oneTimeProductNotification['purchaseToken'] as String;
      final productData = productDataMap[sku]!;
      final result = await handleNonSubscription(
        userId: null,
        productData: productData,
        token: purchaseToken,
      );
      if (result && ackId != null) {
        await _ackMessage(ackId);
      }
    } else {
      print('invalid data');
    }
  }

  /// ACK Messages from Pub/Sub
  Future<void> _ackMessage(String id) async {
    print('ACK Message');
    final request = pubsub.AcknowledgeRequest(
      ackIds: [id],
    );
    final subscriptionName =
        'projects/$googlePlayProjectName/subscriptions/$googlePlayPubsubBillingTopic-sub';
    await pubsubApi.projects.subscriptions.acknowledge(
      request,
      subscriptionName,
    );
  }

O código que você acabou de adicionar se comunica com o tópico do Pub/Sub do Google Cloud a cada dez segundos e pede novas mensagens. Em seguida, processa cada mensagem no método _processMessage.

Esse método decodifica as mensagens recebidas e recebe as informações atualizadas sobre cada compra, tanto de assinaturas quanto de não assinaturas, chamando o handleSubscription ou o handleNonSubscription, se necessário.

Cada mensagem precisa ser confirmada com o método _askMessage.

Em seguida, adicione as dependências necessárias ao arquivo server.dart. Adicione PubsubApi.cloudPlatformScope à configuração de credenciais:

bin/server.dart

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

Depois, crie a instância PubsubApi:

bin/server.dart

  final pubsubApi = pubsub.PubsubApi(clientGooglePlay);

Por fim, transmita-o para o construtor GooglePlayPurchaseHandler:

bin/server.dart

  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi, // new
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
    ),
  };

Configuração do Google Play

Você escreveu o código para consumir eventos de faturamento do tópico do Pub/Sub, mas não criou esse tópico nem está publicando eventos de faturamento. É hora de definir isso.

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

  1. Acesse a página do Cloud Pub/Sub no console do Google Cloud.
  2. Verifique se você está no projeto do Firebase e clique em + Criar tópico. d5ebf6897a0a8bf5.png
  3. Dê um nome ao novo tópico, idêntico ao valor definido para GOOGLE_PLAY_PUBSUB_BILLING_TOPIC em constants.ts. Neste caso, dê o nome play_billing a ela. Se você escolher outra opção, atualize constants.ts. Crie o tópico. 20d690fc543c4212.png
  4. Na lista dos seus tópicos do Pub/Sub, clique nos três pontos verticais do item que você acabou de criar e clique em Ver permissões. ea03308190609fb.png
  5. Na barra lateral à direita, escolha Adicionar principal.
  6. Aqui, adicione google-play-developer-notifications@system.gserviceaccount.com e conceda a ele o papel de Editor do Pub/Sub. 55631ec0549215bc.png
  7. Salve as mudanças nas permissões.
  8. Copie o Nome do tópico que você acabou de criar.
  9. Abra o Play Console novamente e escolha o app na lista Todos os apps.
  10. Role a tela para baixo e vá até Gerar receita > Configuração de monetização.
  11. Preencha o tópico completo e salve as alterações. 7e5e875dc6ce5d54.png

Todos os eventos de faturamento do Google Play agora serão publicados nesse 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 gerenciamento de atualizações em compras na App Store. Uma delas é implementar um webhook que você fornece à Apple e ela usa para se comunicar com seu servidor. A segunda maneira, que você vai encontrar neste codelab, é se conectar à API App Store Server e acessar as informações de assinatura manualmente.

Este codelab se concentra na segunda solução porque você teria que expor seu servidor à Internet para implementar o webhook.

Em um ambiente de produção, o ideal é ter os dois. O webhook para receber eventos da App Store e a API Server, caso você tenha perdido um evento ou precise verificar novamente o status da assinatura.

Comece abrindo lib/app_store_purchase_handler.dart e adicionando a dependência AppStoreServerAPI:

lib/app_store_purchase_handler.dart

final AppStoreServerAPI appStoreServerAPI;

AppStorePurchaseHandler(
  this.iapRepository,
  this.appStoreServerAPI, // new
)

Modifique o construtor para adicionar um timer que vai chamar o método _pullStatus. Esse timer 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();
    });
  }

Depois, crie o método _pullStatus da seguinte maneira:

lib/app_store_purchase_handler.dart

  Future<void> _pullStatus() async {
    print('Polling App Store');
    final purchases = await iapRepository.getPurchases();
    // filter for App Store subscriptions
    final appStoreSubscriptions = purchases.where((element) =>
        element.type == ProductType.subscription &&
        element.iapSource == IAPSource.appstore);
    for (final purchase in appStoreSubscriptions) {
      final status =
          await appStoreServerAPI.getAllSubscriptionStatuses(purchase.orderId);
      // Obtain all subscriptions for the order id.
      for (final subscription in status.data) {
        // Last transaction contains the subscription status.
        for (final transaction in subscription.lastTransactions) {
          final expirationDate = DateTime.fromMillisecondsSinceEpoch(
              transaction.transactionInfo.expiresDate ?? 0);
          // Check if subscription has expired.
          final isExpired = expirationDate.isBefore(DateTime.now());
          print('Expiration Date: $expirationDate - isExpired: $isExpired');
          // Update the subscription status with the new expiration date and status.
          await iapRepository.updatePurchase(SubscriptionPurchase(
            userId: null,
            productId: transaction.transactionInfo.productId,
            iapSource: IAPSource.appstore,
            orderId: transaction.originalTransactionId,
            purchaseDate: DateTime.fromMillisecondsSinceEpoch(
                transaction.transactionInfo.originalPurchaseDate),
            type: ProductType.subscription,
            expiryDate: expirationDate,
            status: isExpired
                ? SubscriptionStatus.expired
                : SubscriptionStatus.active,
          ));
        }
      }
    }
  }

Esse método funciona da seguinte maneira:

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

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

bin/server.dart

  // add from here
  final subscriptionKeyAppStore =
      File('assets/SubscriptionKey.p8').readAsStringSync();

  // Configure Apple Store API access
  var appStoreEnvironment = AppStoreEnvironment.sandbox(
    bundleId: bundleId,
    issuerId: appStoreIssuerId,
    keyId: appStoreKeyId,
    privateKey: subscriptionKeyAppStore,
  );

  // Stored token for Apple Store API access, if available
  final file = File('assets/appstore.token');
  String? appStoreToken;
  if (file.existsSync() && file.lengthSync() > 0) {
    appStoreToken = file.readAsStringSync();
  }

  final appStoreServerAPI = AppStoreServerAPI(
    AppStoreServerHttpClient(
      appStoreEnvironment,
      jwt: appStoreToken,
      jwtTokenUpdatedCallback: (token) {
        file.writeAsStringSync(token);
      },
    ),
  );
  // to here


  return {
    'google_play': GooglePlayPurchaseHandler(
      androidPublisher,
      iapRepository,
      pubsubApi,
    ),
    'app_store': AppStorePurchaseHandler(
      iapRepository,
      appStoreServerAPI, // new
    ),
  };

Configuração da App Store

Em seguida, configure a App Store:

  1. Faça login no App Store Connect e selecione Usuários e acesso.
  2. Vá para Tipo de chave > Compras no app.
  3. Toque no sinal de mais para adicionar um novo.
  4. Forneça um nome, por exemplo, "Chave do codelab".
  5. Faça o download do arquivo p8 que contém a chave.
  6. Copie-o 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 na parte de cima da lista de chaves e defina-o como a constante appStoreIssuerId no arquivo lib/constants.dart.

9540ea9ada3da151.png

Acompanhar compras no dispositivo

A maneira mais segura de rastrear suas compras é no servidor, porque o cliente é difícil de proteger, mas você precisa ter alguma forma de retornar as informações ao cliente para que o app possa agir de acordo com as informações de status da assinatura. Ao armazenar as compras no Firestore, é possível sincronizar facilmente os dados com o cliente e mantê-los atualizados automaticamente.

Você já incluiu o IAPRepo no app, que é o repositório do Firestore que contém todos os dados de compra do usuário no List<PastPurchase> purchases. O repositório também contém hasActiveSubscription,, que é verdadeiro quando há uma compra com productId storeKeySubscription com um status que não expirou. Quando o usuário não está conectado, a lista fica vazia.

lib/repo/iap_repo.dart

  void updatePurchases() {
    _purchaseSubscription?.cancel();
    var user = _user;
    if (user == null) {
      purchases = [];
      hasActiveSubscription = false;
      hasUpgrade = false;
      return;
    }
    var purchaseStream = _firestore
        .collection('purchases')
        .where('userId', isEqualTo: user.uid)
        .snapshots();
    _purchaseSubscription = purchaseStream.listen((snapshot) {
      purchases = snapshot.docs.map((DocumentSnapshot document) {
        var data = document.data();
        return PastPurchase.fromJson(data);
      }).toList();

      hasActiveSubscription = purchases.any((element) =>
          element.productId == storeKeySubscription &&
          element.status != Status.expired);

      hasUpgrade = purchases.any(
        (element) => element.productId == storeKeyUpgrade,
      );

      notifyListeners();
    });
  }

Toda a lógica de compra está na classe DashPurchases e é onde as assinaturas precisam ser aplicadas ou removidas. Portanto, adicione o iapRepo como uma propriedade na classe e atribua o iapRepo no construtor. Em seguida, adicione diretamente um listener no construtor e remova-o no método dispose(). A princípio, 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

  IAPRepo iapRepo;

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

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

  void purchasesUpdate() {
    //TODO manage updates
  }

Em seguida, forneça o IAPRepo ao construtor no main.dart.. Você pode acessar o repositório usando context.read, porque ele já foi criado em um Provider.

lib/main.dart

        ChangeNotifierProvider<DashPurchases>(
          create: (context) => DashPurchases(
            context.read<DashCounter>(),
            context.read<FirebaseNotifier>(),
            context.read<IAPRepo>(),
          ),
          lazy: false,
        ),

Em seguida, escreva o código da função purchaseUpdate(). Em dash_counter.dart,, os métodos applyPaidMultiplier e removePaidMultiplier definem o multiplicador como 10 ou 1, respectivamente, para que você não precise verificar se a assinatura já foi aplicada. Quando o status da assinatura muda, você também atualiza o status do produto à venda para que possa 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();
    }
  }

Você já garantiu que o status da assinatura e do upgrade esteja sempre atualizado no serviço de back-end e sincronizado com o aplicativo. O app age de acordo e aplica os recursos de assinatura e upgrade ao jogo de clicar da Dash.

12. Pronto!

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

Para saber mais, veja os outros codelabs do Flutter (link em inglês).