Processar códigos de resposta de BillingResult

Quando uma chamada da Biblioteca Play Faturamento gera uma ação, a biblioteca retorna uma resposta BillingResult para informar o resultado aos desenvolvedores. Por exemplo, se você usar queryProductDetailsAsync para receber as ofertas disponíveis para o usuário, o código de resposta vai conter um código OK e fornecer o objeto ProductDetails correto. Ele também pode conter uma resposta diferente que indica por que o objeto ProductDetails não pôde ser fornecido.

Nem todos os códigos de resposta são erros. A página de referência BillingResponseCode fornece uma descrição detalhada de cada uma das respostas discutidas neste guia. Confira alguns exemplos de códigos de resposta que não indicam erros:

Quando o código de resposta indica um erro, a causa pode ser condições temporárias. Portanto, a recuperação é possível. Quando uma chamada para um método da Biblioteca Play Faturamento retorna um valor BillingResponseCode que indica uma condição recuperável, é necessário tentar fazer a chamada novamente. Em outros casos, as condições não são consideradas temporárias, então não recomendamos uma nova tentativa.

Erros temporários exigem estratégias diferentes de nova tentativa dependendo de fatores como se o erro ocorre quando os usuários estão em sessão (por exemplo, ao passar por um fluxo de compra) ou em segundo plano (por exemplo, quando você consulta as compras do usuário durante onResume). A seção de estratégias de nova tentativa abaixo mostra exemplos dessas estratégias diferentes, e a seção "Respostas de BillingResult que aceitam novas tentativas" recomenda qual estratégia funciona melhor para cada código de resposta.

Além do código, algumas respostas de erro incluem mensagens para fins de depuração e geração de registros.

Estratégias de nova tentativa

Nova tentativa simples

Em situações em que o usuário está em sessão, é melhor implementar uma estratégia de nova tentativa simples para que o erro interfira o mínimo possível na experiência. Recomendamos essa estratégia com um número máximo de tentativas como condição de saída.

O exemplo a seguir demonstra uma estratégia de nova tentativa simples para lidar com um erro ao estabelecer uma conexão BillingClient:

class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
  // Initialize the BillingClient.
  private val billingClient = BillingClient.newBuilder(context)
    .setListener(this)
    .enablePendingPurchases()
    .build()

  // Establish a connection to Google Play.
  fun startBillingConnection() {
    billingClient.startConnection(object : BillingClientStateListener {
      override fun onBillingSetupFinished(billingResult: BillingResult) {
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
          Log.d(TAG, "Billing response OK")
          // The BillingClient is ready. You can now query Products Purchases.
        } else {
          Log.e(TAG, billingResult.debugMessage)
          retryBillingServiceConnection()
        }
      }

      override fun onBillingServiceDisconnected() {
        Log.e(TAG, "GBPL Service disconnected")
        retryBillingServiceConnection()
      }
    })
  }

  // Billing connection retry logic. This is a simple max retry pattern
  private fun retryBillingServiceConnection() {
    val maxTries = 3
    var tries = 1
    var isConnectionEstablished = false
    do {
      try {
        billingClient.startConnection(object : BillingClientStateListener {
          override fun onBillingSetupFinished(billingResult: BillingResult) {
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
              isConnectionEstablished = true
              Log.d(TAG, "Billing connection retry succeeded.")
            } else {
              Log.e(
                TAG,
                "Billing connection retry failed: ${billingResult.debugMessage}"
              )
            }
          }
        })
      } catch (e: Exception) {
        e.message?.let { Log.e(TAG, it) }
        tries++
      }
    } while (tries <= maxTries && !isConnectionEstablished)
  }
  ...
}

Nova tentativa com espera exponencial

Recomendamos o uso de espera exponencial para operações da Biblioteca Play Faturamento que aconteçam em segundo plano e não afetem a experiência do usuário durante a sessão.

Por exemplo, seria apropriado implementar isso ao confirmar novas compras, porque essa operação pode acontecer em segundo plano e a confirmação não precisa acontecer em tempo real se ocorrer um erro.

private fun acknowledge(purchaseToken: String): BillingResult {
  val params = AcknowledgePurchaseParams.newBuilder()
    .setPurchaseToken(purchaseToken)
    .build()
  var ackResult = BillingResult()
  billingClient.acknowledgePurchase(params) { billingResult ->
    ackResult = billingResult
  }
  return ackResult
}

suspend fun acknowledgePurchase(purchaseToken: String) {

  val retryDelayMs = 2000L
  val retryFactor = 2
  val maxTries = 3

  withContext(Dispatchers.IO) {
    acknowledge(purchaseToken)
  }

  AcknowledgePurchaseResponseListener { acknowledgePurchaseResult ->
    val playBillingResponseCode =
    PlayBillingResponseCode(acknowledgePurchaseResult.responseCode)
    when (playBillingResponseCode) {
      BillingClient.BillingResponseCode.OK -> {
        Log.i(TAG, "Acknowledgement was successful")
      }
      BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
        // This is possibly related to a stale Play cache.
        // Querying purchases again.
        Log.d(TAG, "Acknowledgement failed with ITEM_NOT_OWNED")
        billingClient.queryPurchasesAsync(
          QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
        )
        { billingResult, purchaseList ->
          when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
              purchaseList.forEach { purchase ->
                acknowledge(purchase.purchaseToken)
              }
            }
          }
        }
      }
      in setOf(
         BillingClient.BillingResponseCode.ERROR,
         BillingClient.BillingResponseCode.SERVICE_DISCONNECTED,
         BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE,
       ) -> {
        Log.d(
          TAG,
          "Acknowledgement failed, but can be retried --
          Response Code: ${acknowledgePurchaseResult.responseCode} --
          Debug Message: ${acknowledgePurchaseResult.debugMessage}"
        )
        runBlocking {
          exponentialRetry(
            maxTries = maxTries,
            initialDelay = retryDelayMs,
            retryFactor = retryFactor
          ) { acknowledge(purchaseToken) }
        }
      }
      in setOf(
         BillingClient.BillingResponseCode.BILLING_UNAVAILABLE,
         BillingClient.BillingResponseCode.DEVELOPER_ERROR,
         BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED,
       ) -> {
        Log.e(
          TAG,
          "Acknowledgement failed and cannot be retried --
          Response Code: ${acknowledgePurchaseResult.responseCode} --
          Debug Message: ${acknowledgePurchaseResult.debugMessage}"
        )
        throw Exception("Failed to acknowledge the purchase!")
      }
    }
  }
}

private suspend fun <T> exponentialRetry(
  maxTries: Int = Int.MAX_VALUE,
  initialDelay: Long = Long.MAX_VALUE,
  retryFactor: Int = Int.MAX_VALUE,
  block: suspend () -> T
): T? {
  var currentDelay = initialDelay
  var retryAttempt = 1
  do {
    runCatching {
      delay(currentDelay)
      block()
    }
      .onSuccess {
        Log.d(TAG, "Retry succeeded")
        return@onSuccess;
      }
      .onFailure { throwable ->
        Log.e(
          TAG,
          "Retry Failed -- Cause: ${throwable.cause} -- Message: ${throwable.message}"
        )
      }
    currentDelay *= retryFactor
    retryAttempt++
  } while (retryAttempt < maxTries)

  return block() // last attempt
}

Respostas de BillingResult que aceitam novas tentativas

NETWORK_ERROR (código do erro: 12)

Problema

Esse erro indica que houve um problema com a conexão de rede entre o dispositivo e os sistemas do Google Play.

Possível solução

Para recuperar, use novas tentativas simples ou com espera exponencial, dependendo de qual ação gerou o erro.

SERVICE_TIMEOUT (código do erro: -3)

Problema

Esse erro indica que a solicitação atingiu o tempo limite máximo antes de o Google Play responder. Isso pode ser causado, por exemplo, por um atraso na execução da ação solicitada pela chamada da Biblioteca Play Faturamento.

Possível solução

Esse geralmente é um problema temporário. Tente fazer a solicitação outra vez usando uma estratégia simples ou de espera exponencial, dependendo da ação que retornou o erro.

Ao contrário do SERVICE_DISCONNECTED abaixo, a conexão com o serviço do Google Play Faturamento não é interrompida, e você só precisa repetir a operação da biblioteca.

SERVICE_DISCONNECTED (código do erro: -1)

Problema

Esse erro fatal indica que a conexão do app cliente com o serviço da Google Play Store pelo BillingClient foi interrompida.

Possível solução

Para evitar esse erro o máximo possível, sempre verifique a conexão com o Google Play Services antes de fazer chamadas com a Biblioteca Play Faturamento chamando BillingClient.isReady().

Para recuperar o SERVICE_DISCONNECTED, o app cliente precisa tentar restabelecer a conexão usando BillingClient.startConnection.

Assim como com o SERVICE_TIMEOUT, use novas tentativas simples ou com espera exponencial, dependendo de qual ação gerou o erro.

SERVICE_UNAVAILABLE (código do erro: 2)

Observação importante:

A partir da Biblioteca Google Play Faturamento 6.0.0, o erro SERVICE_UNAVAILABLE não é mais retornado para problemas de rede. Ele é retornado quando o serviço de faturamento está indisponível e os cenários de caso SERVICE_TIMEOUT foram descontinuados.

Problema

Esse erro temporário indica que o serviço do Google Play Faturamento está indisponível. Na maioria dos casos, isso significa que há um problema de conexão de rede em algum lugar entre o dispositivo cliente e os serviços do Google Play Faturamento.

Possível solução

Esse geralmente é um problema temporário. Tente fazer a solicitação outra vez usando uma estratégia simples ou de espera exponencial, dependendo da ação que retornou o erro.

Ao contrário do SERVICE_DISCONNECTED, a conexão com o serviço do Google Play Faturamento não é interrompida, e você precisa repetir a operação.

BILLING_UNAVAILABLE (código do erro: 3)

Problema

Esse erro indica que ocorreu um problema no faturamento do usuário durante o processo de compra. Confira alguns exemplos de quando isso pode ocorrer:

  • O app Play Store no dispositivo do usuário está desatualizado.
  • O usuário está em um país onde não há suporte.
  • Esse é um usuário corporativo, e o administrador dele desativou a possibilidade de usuários fazerem compras.
  • O Google Play não consegue fazer a cobrança na forma de pagamento do usuário. Por exemplo, o cartão de crédito do usuário pode ter expirado.

Possível solução

É improvável que novas tentativas automáticas ajudem neste caso. No entanto, uma nova tentativa manual pode ajudar se o usuário resolver a condição que causou o problema. Por exemplo, se o usuário atualizar a Play Store para uma versão com suporte, uma nova tentativa manual da operação inicial poderá funcionar.

Se esse erro ocorrer quando o usuário não estiver em sessão, é possível que a nova tentativa não faça sentido Quando você recebe um erro BILLING_UNAVAILABLE como resultado do fluxo de compra, é muito provável que o usuário tenha recebido feedback do Google Play durante o processo de compra e esteja ciente do que deu errado. Nesse caso, você pode mostrar uma mensagem de erro especificando que algo deu errado e incluir um botão "Tente de novo" para dar ao usuário a opção de uma nova tentativa manual depois de resolver o problema.

ERROR (código do erro: 6)

Problema

Esse é um erro fatal que indica um problema interno com o Google Play.

Possível solução

Às vezes, problemas internos do Google Play que levam a um ERROR são temporários, e uma nova tentativa com uma espera exponencial pode ser implementada para fazer a mitigação. Quando os usuários estão em sessão, é preferível fazer uma nova tentativa simples.

ITEM_ALREADY_OWNED

Problema

Essa resposta indica que o usuário do Google Play já é proprietário da assinatura ou do produto de compra única que ele está tentando comprar. Na maioria dos casos, esse não é um erro temporário, exceto quando causado por um cache desatualizado do Google Play.

Possível solução

Para evitar esse erro quando a causa não for um problema de cache, não ofereça um produto para compra quando o usuário já for proprietário dele. Verifique os direitos do usuário ao mostrar os produtos disponíveis para compra e filtre o que ele pode comprar de acordo com esses direitos. Quando o app cliente recebe esse erro devido a um problema de cache, ele aciona o cache do Google Play para ser atualizado com os dados mais recentes do back-end. Tentar de novo após o erro deve resolver essa instância temporária específica nesse caso. Chame BillingClient.queryPurchasesAsync() depois de receber um ITEM_ALREADY_OWNED para verificar se o usuário adquiriu o produto. Se não for o caso, implemente uma lógica de nova tentativa simples para tentar fazer a compra outra vez.

ITEM_NOT_OWNED

Problema

Essa resposta de compra indica que o usuário do Google Play não é proprietário da assinatura ou do produto de compra única que está tentando substituir, confirmar ou consumir. Na maioria dos casos, esse não é um erro temporário, exceto quando é causado por um cache desatualizado do Google Play.

Possível solução

Quando o erro é recebido devido a um problema de cache, ele aciona o cache do Google Play para ser atualizado com os dados mais recentes do back-end. Tentar de novo com uma estratégia simples após o erro deve resolver essa instância temporária específica. Chame BillingClient.queryPurchasesAsync() depois de receber um ITEM_NOT_OWNED para verificar se o usuário adquiriu o produto. Se não for o caso, use uma lógica simples para tentar fazer a compra outra vez.

Respostas de BillingResult que não aceitam novas tentativas

Não é possível se recuperar desses erros usando a lógica de nova tentativa.

FEATURE_NOT_SUPPORTED

Problema

Esse erro que não aceita novas tentativas indica que o recurso do Google Play Faturamento não tem suporte ao dispositivo do usuário, provavelmente devido a uma versão antiga da Play Store.

Por exemplo, talvez alguns dispositivos dos usuários não tenham suporte a mensagens no app.

Possível mitigação

Use BillingClient.isFeatureSupported() para conferir o suporte a recursos antes de fazer uma chamada para a Biblioteca Play Faturamento.

when {
  billingClient.isReady -> {
    if (billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)) {
       // use feature
    }
  }
}

USER_CANCELED

Problema

O usuário clicou para sair da interface do fluxo de faturamento.

Possível solução

Esse erro é apenas informativo, e falhas podem ocorrer sem maiores problemas.

ITEM_UNAVAILABLE

Problema

O produto de compra única ou a assinatura do Google Play Faturamento não está disponível para compra por esse usuário.

Possível mitigação

Confira se o app atualiza os detalhes do produto usando queryProductDetailsAsync, conforme recomendado. Considere a frequência com que seu catálogo de produtos muda na configuração do Play Console para implementar mais atualizações, caso necessário. Tente vender produtos no Google Play Faturamento que retornem as informações corretas com a queryProductDetailsAsync. Verifique se há inconsistências na configuração de qualificação do produto. Por exemplo, você pode estar consultando um produto que está disponível apenas para uma região diferente daquela em que o usuário está tentando comprar. Caso você queira disponibilizar um produto para compra, ele precisa estar ativo, e o app precisa estar publicado e disponível no país do usuário.

Às vezes, principalmente durante o teste, tudo está correto na configuração do produto, mas os usuários ainda recebem esse erro. Isso pode ocorrer devido a um atraso de propagação dos detalhes do produto nos servidores do Google. Tente de novo mais tarde.

DEVELOPER_ERROR

Problema

Esse é um erro fatal que indica o uso inadequado de uma API. Por exemplo, fornecer parâmetros incorretos para BillingClient.launchBillingFlow pode causar esse erro.

Possível solução

Confira se você está usando corretamente as diferentes chamadas da Biblioteca Play Faturamento. Além disso, confira a mensagem de depuração para saber mais sobre o erro.