---
title: "Compras no aplicativo para Android, parte 2: processamento de compras com a Biblioteca Google Play de Faturamento."
description: "Como processar as compras e administrar uma mudança de assinatura."
url: "https://adapty.io/pt/blog/android-in-app-purchases-google-play-billing-library-part-2/"
language: "pt"
slug: "android-in-app-purchases-google-play-billing-library-part-2"
category: "adapty-updates"
date: "2021-03-24T00:00:00.000Z"
date_modified: "2021-03-24T00:00:00.000Z"
author: "Vlad Guriev"
---

# Compras no aplicativo para Android, parte 2: processamento de compras com a Biblioteca Google Play de Faturamento.

No artigo anterior, criamos uma classe de wrapper para trabalhar com a Biblioteca de Faturamento:

```kotlin
import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
   interface OnQueryProductsListener {
       fun onSuccess(products: List<SkuDetails>)
       fun onFailure(error: Error)
   }
   class Error(val responseCode: Int, val debugMessage: String)
   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()
   fun queryProducts(listener: OnQueryProductsListener) {
       val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
       queryProductsForType(
           skusList,
           BillingClient.SkuType.SUBS
       ) { billingResult, skuDetailsList ->
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               val products = skuDetailsList ?: mutableListOf()
               queryProductsForType(
                   skusList,
                   BillingClient.SkuType.INAPP
               ) { billingResult, skuDetailsList ->
                   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                       products.addAll(skuDetailsList ?: listOf())
                       listener.onSuccess(products)
                   } else {
                       listener.onFailure(
                           Error(billingResult.responseCode, billingResult.debugMessage)
                       )
                   }
               }
           } else {
               listener.onFailure(
                   Error(billingResult.responseCode, billingResult.debugMessage)
               )
           }
       }
   }
   private fun queryProductsForType(
       skusList: List<String>,
       @BillingClient.SkuType type: String,
       listener: SkuDetailsResponseListener
   ) {
       onConnected {
           billingClient.querySkuDetailsAsync(
               SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
               listener
           )
       }
   }
   private fun onConnected(block: () -> Unit) {
       billingClient.startConnection(object : BillingClientStateListener {
           override fun onBillingSetupFinished(billingResult: BillingResult) {
               block()
           }
           override fun onBillingServiceDisconnected() {
               // Try to restart the connection on the next request to
               // Google Play by calling the startConnection() method.
           }
       })
   }
   override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
       // here come callbacks about new purchases
   }
}
```

Vamos prosseguir com a implementação da compra e melhorar nossa classe.

## Como criar uma tela de assinatura

Qualquer aplicativo que apresente compras no aplicativo tem uma tela de paywall. Existem [políticas do Google](https://support.google.com/googleplay/android-developer/answer/9900533) que definem o número mínimo de elementos e textos instrucionais que devem estar presentes nas telas. Segue um resumo. Na tela do paywall, é necessário ser explícito sobre as condições de assinatura, custos e duração, assim como especificar se a assinatura é necessária para utilizar o aplicativo (app). Deve-se também evitar forçar seus usuários a realizar qualquer ação adicional para rever as condições.

Vamos usar como exemplo uma tela de paywall simplificada:

![631abc67c79cf6614ff8de49 3321](https://adapty.io/wp-content/uploads/2023/01/631abc67c79cf6614ff8de49_3321.png)
*Exemplo de paywall no Android*

Temos os seguintes elementos na tela do paywall:

- Um cabeçalho.
- Botões definidos para iniciar o processo de compra. Estes botões orientam o usuário sobre os detalhes gerais das opções de assinatura, como o título e os custos, que são exibidas na moeda local.
- Texto de instrução.
- Um botão para restaurar a compra feita anteriormente. Este elemento é obrigatório para qualquer aplicativo que apresente assinaturas ou compras não consumíveis.

Ajuste do código para exibir as informações do produto

Há quatro produtos no nosso exemplo:

- Duas assinaturas com renovação automática (“premium_sub_month” e “premium_sub_year”);
- Um produto que não pode ser comprado duas vezes ou um produto não consumível (“unlock_feature”);
- Um produto que pode ser comprado várias vezes ou um produto consumível (“coin_pack_large”).

Para simplificar o exemplo, usaremos uma Atividade que iremos injetar com o BillingClientWrapper criado no artigo anterior, assim como um layout com um número fixo de botões de compra.

```kotlin
class PaywallActivity: BaseActivity() {

   @Inject
   lateinit var billingClientWrapper: BillingClientWrapper

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_paywall)

       displayProducts() //to be declared below
   }
}
```

Por comodidade, vamos adicionar um mapa onde a chave é o [sku](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getSku%28)) do produto, e o valor é o botão correspondente na tela.

```kotlin
private val purchaseButtonsMap: Map<String, Button> by lazy(LazyThreadSafetyMode.NONE) {
   mapOf(
       "premium_sub_month" to monthlySubButton,
       "premium_sub_year" to yearlySubButton,
       "coin_pack_large" to coinPackLargeButton,
       "unlock_feature" to unlockFeatureButton,
   )
}
```

Vamos declarar um método para exibir os produtos na IU. Ele será baseado na lógica introduzida em nosso tutorial [anterior](https://adapty.io/blog/android-in-app-subscriptions-part-1/).

```kotlin
private fun displayProducts() {
   billingClientWrapper.queryProducts(object : BillingClientWrapper.OnQueryProductsListener {
       override fun onSuccess(products: List<SkuDetails>>) {
           products.forEach { product ->
               purchaseButtonsMap[product.sku]?.apply {
                   text = "${product.description} for ${product.price}"
                   setOnClickListener {
                       billingClientWrapper.purchase(this@PaywallActivity, product) //will be declared below
                   }
               }
           }
       }

       override fun onFailure(error: BillingClientWrapper.Error) {
           //handle error
       }
   })
}
```

product.price é uma sequência já formatada que especifica a moeda local para a conta. não precisa de nenhuma formatação adicional. Todas as outras propriedades do objeto da classe [SkuDetails](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails) também são totalmente localizadas.

## Como iniciar o processo de compra

Para processar a compra, temos que usar o método [launchBillingFlow()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchBillingFlow%28android.app.Activity,%20com.android.billingclient.api.BillingFlowParams)) do thread principal do aplicativo.

Para isso, vamos adicionar um método de compra() ao BillingClientWrapper.

```kotlin
fun purchase(activity: Activity, product: SkuDetails) {
   onConnected {
       activity.runOnUiThread {
           billingClient.launchBillingFlow(
               activity,
               BillingFlowParams.newBuilder().setSkuDetails(product).build()
           )
       }
   }
}
```

O método launchBillingFlow() não tem retorno de chamada. A resposta retornará ao método onPurchasesUpdated(). Você se lembra que citamos isso [no artigo anterior](https://adapty.io/blog/android-in-app-subscriptions-part-1/) e pedimos guardar a informação para mais tarde? Bem, vamos precisar dela agora.

O método onPurchasesUpdated() é chamado sempre que houver algum resultado fora da interação do usuário com o diálogo de compra. Pode ser uma compra realizada com sucesso ou um cancelamento de compra causado pelo fechamento da caixa de diálogo por parte do usuário, neste caso obteremos o código [BillingResponseCode.*USER_CANCELED*](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#USER_CANCELED). Outra possibilidade pode ser qualquer uma das outras mensagens de erro possíveis.

De forma semelhante à interface OnQueryProductsListener mencionada no artigo anterior, vamos citar uma interface OnPurchaseListener na classe BillingClientWrapper. Através dessa interface, receberemos a compra (um objeto da classe [Compra](https://developer.android.com/reference/com/android/billingclient/api/Purchase) ou uma mensagem de erro declarada por nós [no guia anterior](https://adapty.io/blog/android-in-app-subscriptions/). No próximo artigo, discutiremos o caso em que o objeto da classe “Compra” pode ser nulo, mesmo que a compra tenha sido realizada com sucesso.

```kotlin
interface OnPurchaseListener {
   fun onPurchaseSuccess(purchase: Purchase?)
   fun onPurchaseFailure(error: Error)
}

var onPurchaseListener: OnPurchaseListener? = null
```

A seguir, vamos implementá-la no PaywallActivity:

```kotlin
class PaywallActivity: BaseActivity(), BillingClientWrapper.OnPurchaseListener {

   @Inject
   lateinit var billingClientWrapper: BillingClientWrapper

   private val purchaseButtonsMap: Map<String, Button> by lazy(LazyThreadSafetyMode.NONE) {
       mapOf(
           "premium_sub_month" to monthlySubButton,
           "premium_sub_year" to yearlySubButton,
           "coin_pack_large" to coinPackLargeButton,
           "unlock_feature" to unlockFeatureButton,
       )
   }

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_paywall)

       billingClientWrapper.onPurchaseListener = this

       displayProducts()
   }

   override fun onPurchaseSuccess(purchase: Purchase?) {
       //handle successful purchase
   }

   override fun onPurchaseFailure(error: BillingClientWrapper.Error) {
       //handle error or user cancelation
   }

   private fun displayProducts() {
       billingClientWrapper.queryProducts(object : BillingClientWrapper.OnQueryProductsListener {
           override fun onSuccess(products: List<SkuDetails>) {
               products.forEach { product ->
                   purchaseButtonsMap[product.sku]?.apply {
                       text = "${product.description} for ${product.price}"
                       setOnClickListener {
                           billingClientWrapper.purchase(this@PaywallActivity, product)
                       }
                   }
               }
           }

           override fun onFailure(error: BillingClientWrapper.Error) {
               //handle error
           }
       })
   }
}
```

Vamos adicionar alguma lógica ao onPurchaseUpdated():

```kotlin
override fun onPurchasesUpdated(
   billingResult: BillingResult,
   purchaseList: MutableList<Purchase>?
) {
   when (billingResult.responseCode) {
       BillingClient.BillingResponseCode.OK -> {
           if (purchaseList == null) {
               //to be discussed in the next article
               onPurchaseListener?.onPurchaseSuccess(null)
               return
           }

           purchaseList.forEach(::processPurchase) //to be declared below
       }
       else -> {
           //error occured or user canceled
           onPurchaseListener?.onPurchaseFailure(
               BillingClientWrapper.Error(
                   billingResult.responseCode,
                   billingResult.debugMessage
               )
           )
       }
   }
}
```

Caso a purchaseList não esteja vazia, primeiro teremos que escolher os itens cuja purchaseState é igual a [PurchaseState.PURCHASED](https://developer.android.com/google/play/billing/integrate#pending), á que também existem compras [pendentes](https://developer.android.com/google/play/billing/integrate#pending). Se for esse o caso, o fluxo de usuários termina aqui. De acordo com os [documentos](https://developer.android.com/google/play/billing/integrate#process), devemos verificar a compra no nosso servidor. Abordaremos este assunto nos próximos artigos desta série. Assim que for concluída a verificação do servidor, você deve entregar o conteúdo e informar o Google. Caso isso não seja feito, a compra será automaticamente reembolsada em três dias. É bastante interessante que esta política seja exclusiva do Google Play. O iOS não impõe nenhuma política semelhante. Você tem duas maneiras de reconhecer a entrega de seu conteúdo ao usuário:

- Através do [acknowledgePurchase()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#acknowledgepurchase) no lado do cliente;
- Através do [Product.Purchases.Acknowledge](https://developers.google.com/android-publisher/api-ref/purchases/products/acknowledge)/[Purchases.Subscriptions.Acknowledge](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/acknowledge) no lado do sistema backend.

Quando se trata de um produto consumível, temos que usar o método [consumeAsync()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#consumeasync). Ele reconhece a compra em detalhes, ao mesmo tempo em que torna possível refazer a compra do produto. Isto só pode ser feito com a Biblioteca de Faturamento. Por alguma razão, a API do Google Play Developer não oferece nenhuma maneira de fazer isso no sistema backend. É bastante curioso que, ao contrário do Google Play, o App Store e o AppGallery configurem o produto como consumível via App Store Connect e AppGallery Connect, respectivamente. No entanto, tais produtos no AppGallery também devem ser consumidos de forma explícita.

Vamos escrever os métodos de reconhecimento e consumo, assim como duas versões do método ProcessPurchase() para contabilizar se estamos ou não executando nosso próprio sistema backend.

```kotlin
private fun acknowledgePurchase(
   purchase: Purchase,
   callback: AcknowledgePurchaseResponseListener
) {
   onConnected {
       billingClient.acknowledgePurchase(
           AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
               .build(),
           callback::onAcknowledgePurchaseResponse
       )
   }
}

private fun consumePurchase(purchase: Purchase, callback: ConsumeResponseListener) {
   onConnected {
       billingClient.consumeAsync(
           ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
       ) { billingResult, purchaseToken ->
           callback.onConsumeResponse(billingResult, purchaseToken)
       }
   }
}
```

Sem verificação do servidor:

```kotlin
private fun processPurchase(purchase: Purchase) {
   if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
       onPurchaseListener?.onPurchaseSuccess(purchase)

       if (purchase.skus.firstOrNull() == "coin_pack_large") {
           //consuming our only consumable product
           consumePurchase(purchase) { billingResult, purchaseToken ->
               if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                   //implement retry logic or try to consume again in onResume()
               }
           }
       } else if (!purchase.isAcknowledged) {
           acknowledgePurchase(purchase) { billingResult ->
               if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                   //implement retry logic or try to acknowledge again in onResume()
               }
           }
       }
   }
}
```

Com verificação do servidor:

```kotlin
private fun processPurchase(purchase: Purchase) {
   if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
       api.verifyPurchase(purchase.purchaseToken) { error ->
           if (error != null) {
               onPurchaseListener?.onPurchaseSuccess(purchase)

               if (purchase.skus.firstOrNull() == "coin_pack_large") {
                   //consuming our only consumable product
                   billingClient.consumeAsync(
                       ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
                           .build()
                   ) { billingResult, purchaseToken ->
                       if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                           //implement retry logic or try to consume again in onResume()
                       }
                   }
               }
           } else {
               //handle verification error
           }
       }
   }
}
```

Nos próximos artigos, vamos tratar da verificação do servidor para compras de forma mais detalhada.

Naturalmente, podemos também implementar o reconhecimento no segundo exemplo do lado do cliente. No entanto, caso você possa fazer algo no sistema backend, faça. Caso o reconhecimento ou o processo de consumo gere algum erro, ele não poderá ser ignorado. Caso nenhum deles seja executado no prazo de 3 dias após a compra ter recebido o status [PurchaseState.*PURCHASED*](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#PURCHASED), ele será cancelado e reembolsado. Portanto, se for impossível fazer no sistema backend e você continuar recebendo o erro após uma série de tentativas, existe uma solução confiável. Você vai ter que obter as compras atuais do usuário através de algum método de ciclo de vida, como [onStart() ou onResume()](https://developer.android.com/reference/androidx/lifecycle/Lifecycle.Event), e continuar tentando, esperando que o usuário abra o aplicativo no prazo de 3 dias durante sua conexão com a Internet. :)

Portanto, a versão atual da classe BillingClientWrapper será assim:

```kotlin
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.*

class BillingClientWrapper(context: Context, private val api: Api) : PurchasesUpdatedListener {

   interface OnQueryProductsListener {
       fun onSuccess(products: List<SkuDetails>)
       fun onFailure(error: Error)
   }

   interface OnPurchaseListener {
       fun onPurchaseSuccess(purchase: Purchase?)
       fun onPurchaseFailure(error: Error)
   }

   var onPurchaseListener: OnPurchaseListener? = null

   class Error(val responseCode: Int, val debugMessage: String)

   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()

   fun purchase(activity: Activity, product: SkuDetails) {
       onConnected {
           activity.runOnUiThread {
               billingClient.launchBillingFlow(
                   activity,
                   BillingFlowParams.newBuilder().setSkuDetails(product).build()
               )
           }
       }
   }

   fun queryProducts(listener: OnQueryProductsListener) {
       val skusList = listOf("premium_sub_month", "premium_sub_year", "coin_pack_large", "unlock_feature")

       queryProductsForType(
           skusList,
           BillingClient.SkuType.SUBS
       ) { billingResult, skuDetailsList ->
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               val products = skuDetailsList ?: mutableListOf()
               queryProductsForType(
                   skusList,
                   BillingClient.SkuType.INAPP
               ) { billingResult, skuDetailsList ->
                   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                       products.addAll(skuDetailsList ?: listOf())
                       listener.onSuccess(products)
                   } else {
                       listener.onFailure(
                           Error(billingResult.responseCode, billingResult.debugMessage)
                       )
                   }
               }
           } else {
               listener.onFailure(
                   Error(billingResult.responseCode, billingResult.debugMessage)
               )
           }
       }
   }

   private fun queryProductsForType(
       skusList: List<String>,
       @BillingClient.SkuType type: String,
       listener: SkuDetailsResponseListener
   ) {
       onConnected {
           billingClient.querySkuDetailsAsync(
               SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
               listener
           )
       }
   }

   private fun onConnected(block: () -> Unit) {
       billingClient.startConnection(object : BillingClientStateListener {
           override fun onBillingSetupFinished(billingResult: BillingResult) {
               block()
           }

           override fun onBillingServiceDisconnected() {
               // Try to restart the connection on the next request to
               // Google Play by calling the startConnection() method.
           }
       })
   }

   override fun onPurchasesUpdated(
       billingResult: BillingResult,
       purchaseList: MutableList<Purchase>?
   ) {
       when (billingResult.responseCode) {
           BillingClient.BillingResponseCode.OK -> {
               if (purchaseList == null) {
                   //to be discussed in the next article
                   onPurchaseListener?.onPurchaseSuccess(null)
                   return
               }

               purchaseList.forEach(::processPurchase)
           }
           else -> {
               //error occured or user canceled
               onPurchaseListener?.onPurchaseFailure(
                   BillingClientWrapper.Error(
                       billingResult.responseCode,
                       billingResult.debugMessage
                   )
               )
           }
       }
   }

   private fun processPurchase(purchase: Purchase) {
       if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
           api.verifyPurchase(purchase.purchaseToken) { error ->
               if (error != null) {
                   onPurchaseListener?.onPurchaseSuccess(purchase)

                   if (purchase.skus.firstOrNull() == "coin_pack_large") {
                       //consuming our only consumable product
                       billingClient.consumeAsync(
                           ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
                               .build()
                       ) { billingResult, purchaseToken ->
                           if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
                               //implement retry logic or try to consume again in onResume()
                           }
                       }
                   }
               } else {
                   //handle verification error
               }
           }
       }
   }

   private fun acknowledgePurchase(
       purchase: Purchase,
       callback: AcknowledgePurchaseResponseListener
   ) {
       onConnected {
           billingClient.acknowledgePurchase(
               AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
                   .build(),
               callback::onAcknowledgePurchaseResponse
           )
       }
   }

   private fun consumePurchase(purchase: Purchase, callback: ConsumeResponseListener) {
       onConnected {
           billingClient.consumeAsync(
               ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build()
           ) { billingResult, purchaseToken ->
               callback.onConsumeResponse(billingResult, purchaseToken)
           }
       }
   }
}
```

Talvez você pergunte por que os botões estão ativos para todos os produtos, não importando se o usuário já os comprou ou não. Ou talvez você tenha algumas perguntas sobre o que acontece se você comprar as duas assinaturas. A primeira assinatura substituirá a segunda, ou elas vão coexistir? Confira os próximos artigos para obter todas as respostas :)
