Gérer les codes de réponse BillingResult

Lorsqu'un appel de la bibliothèque Play Billing déclenche une action, la bibliothèque renvoie une réponse BillingResult pour informer le développeur du résultat. Par exemple, si vous utilisez queryProductDetailsAsync afin d'obtenir les offres disponibles pour l'utilisateur, il y a deux possibilités pour le code de réponse : soit il contient un code OK et fournit l'objet ProductDetails approprié, soit il contient une autre réponse indiquant la raison pour laquelle l'objet ProductDetails n'a pas pu être fourni.

Les codes de réponse ne sont pas tous des erreurs. Sur la page de référence BillingResponseCode, vous trouverez une description détaillée de chacune des réponses abordées dans ce guide. Voici quelques exemples de codes de réponse qui n'indiquent pas d'erreurs :

Lorsque le code de réponse indique une erreur, la cause vient parfois de conditions temporaires, ce qui rend la récupération possible. Lorsqu'un appel à une méthode de la bibliothèque Play Billing renvoie une valeur BillingResponseCode indiquant une condition récupérable, nous vous conseillons de relancer l'appel. Dans d'autres cas, les conditions ne sont pas considérées comme temporaires. Une nouvelle tentative n'est donc pas recommandée.

Les erreurs temporaires requièrent différentes stratégies de nouvelle tentative qui dépendent de facteurs tels que le statut de la session utilisateur au moment de l'erreur (erreur survenant lorsque l'utilisateur suit un parcours d'achat, erreur se produisant en arrière-plan, etc.), par exemple lorsque vous interrogez les achats existants de l'utilisateur pendant onResume. Dans la section Stratégies de nouvelle tentative ci-dessous, vous trouverez des exemples de ces différentes stratégies. Lisez également la section Réponses récupérables BillingResult, dans laquelle nous vous recommandons la stratégie la plus efficace pour chaque code de réponse.

En plus du code de réponse, certaines réponses d'erreur incluent des messages à des fins de débogage et de journalisation.

Stratégies de nouvelle tentative

Nouvelle tentative simple

Lorsqu'une session utilisateur est en cours, il est préférable d'implémenter une stratégie de nouvelle tentative simple afin que l'erreur perturbe le moins possible l'expérience utilisateur. Dans ce cas, nous vous recommandons d'utiliser une stratégie de nouvelle tentative simple, avec un nombre maximal de tentatives comme condition de sortie.

L'exemple suivant illustre une stratégie de nouvelle tentative simple pour gérer une erreur au moment d'établir une connexion 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)
  }
  ...
}

Intervalle exponentiel entre les tentatives

Nous vous recommandons d'utiliser un intervalle exponentiel entre les tentatives pour les opérations de la bibliothèque Play Billing qui se produisent en arrière-plan et qui n'affectent pas l'expérience de l'utilisateur lorsque sa session est en cours.

Par exemple, il est judicieux d'implémenter cette stratégie lorsque l'utilisateur confirme de nouveaux achats, car cette opération peut se produire en arrière-plan. Cette confirmation ne doit pas nécessairement se dérouler en temps réel si une erreur survient.

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
}

Réponses BillingResult récupérables

NETWORK_ERROR (code d'erreur 12)

Problème

Cette erreur indique un problème de connexion réseau entre l'appareil et les systèmes Play.

Solution possible

Pour effectuer la récupération, utilisez des stratégies simples ou d'intervalle exponentiel entre les tentatives, selon l'action qui a déclenché l'erreur.

SERVICE_TIMEOUT (code d'erreur -3)

Problème

Cette erreur indique que la requête a atteint le délai maximal avant que Google Play ne puisse répondre. Cela peut être dû, par exemple, à un retard d'exécution de l'action demandée par l'appel de la bibliothèque Play Billing.

Solution possible

Il s'agit généralement d'un problème temporaire. Relancez la requête à l'aide d'une stratégie simple ou d'intervalle exponentiel entre les tentatives, selon l'action ayant renvoyé l'erreur.

Contrairement à SERVICE_DISCONNECTED ci-dessous, la connexion au service Google Play Billing n'est pas interrompue, et vous devez simplement effectuer une nouvelle tentative pour l'opération souhaitée de la bibliothèque Play Billing.

SERVICE_DISCONNECTED (code d'erreur -1)

Problème

Cette erreur fatale indique que la connexion de l'application cliente au service du Google Play Store via BillingClient a été interrompue.

Solution possible

Pour éviter cette erreur autant que possible, vérifiez toujours la connexion aux services Google Play avant d'effectuer des appels avec la bibliothèque Play Billing en appelant BillingClient.isReady().

Pour tenter d'effectuer le processus de récupération à partir de SERVICE_DISCONNECTED, votre application cliente doit essayer de rétablir la connexion à l'aide de BillingClient.startConnection.

Comme avec SERVICE_TIMEOUT, utilisez des stratégies simples ou d'intervalle exponentiel entre les tentatives, selon l'action qui a déclenché l'erreur.

SERVICE_UNAVAILABLE (code d'erreur 2)

Remarque importante :

À partir de la version 6.0.0 de la bibliothèque Google Play Billing, SERVICE_UNAVAILABLE n'est plus renvoyé en cas de problèmes de réseau. Il est renvoyé lorsque le service de facturation est indisponible et dans les cas de SERVICE_TIMEOUT obsolètes.

Problème

Cette erreur temporaire indique que le service Google Play Billing est actuellement indisponible. Dans la plupart des cas, cela signifie qu'il y a un problème de connexion réseau entre l'appareil client et les services Google Play Billing.

Solution possible

Il s'agit généralement d'un problème temporaire. Relancez la requête à l'aide d'une stratégie simple ou d'intervalle exponentiel entre les tentatives, selon l'action ayant renvoyé l'erreur.

Contrairement à SERVICE_DISCONNECTED, la connexion au service Google Play Billing n'est pas interrompue, et vous devez effectuer une nouvelle tentative pour l'opération souhaitée.

BILLING_UNAVAILABLE (code d'erreur 3)

Problème

Cette erreur indique que l'utilisateur a subi une erreur de facturation lors du processus d'achat. Voici quelques exemples de situations où ce problème peut survenir :

  • L'application Play Store sur l'appareil de l'utilisateur est obsolète.
  • L'utilisateur se trouve dans un pays où le service n'est pas disponible.
  • L'utilisateur est un utilisateur de la version Enterprise, et son administrateur d'entreprise ne l'autorise pas à effectuer des achats.
  • Google Play ne peut pas débiter le mode de paiement de l'utilisateur. Par exemple, sa carte de crédit est peut-être arrivée à expiration.

Solution possible

Dans ce cas, il est peu probable que les nouvelles tentatives automatiques permettent de remédier à la situation. Toutefois, une nouvelle tentative manuelle peut être utile si l'utilisateur résout la condition à l'origine du problème. Par exemple, si l'utilisateur met à jour sa version du Play Store vers une version compatible, une nouvelle tentative manuelle de l'opération initiale peut fonctionner.

Si cette erreur se produit lorsque la session utilisateur n'est pas en cours, une nouvelle tentative n'est pas forcément judicieuse. Lorsque vous obtenez une erreur BILLING_UNAVAILABLE liée au parcours d'achat, l'utilisateur a très probablement reçu des informations de Google Play au cours du processus d'achat et sait peut-être ce qu'il s'est passé. Dans ce cas, vous pouvez afficher un message d'erreur indiquant qu'un problème est survenu et proposer un bouton "Réessayer" pour permettre à l'utilisateur d'effectuer une nouvelle tentative manuelle après avoir résolu le problème.

ERROR (code d'erreur 6)

Problème

Il s'agit d'une erreur fatale qui indique un problème interne à Google Play.

Solution possible

Parfois, les problèmes internes à Google Play qui entraînent ERROR sont temporaires, et une stratégie d'intervalle exponentiel entre les tentatives peut être implémentée pour atténuer les choses. Lorsqu'une session utilisateur est en cours, une nouvelle tentative simple est préférable.

ITEM_ALREADY_OWNED

Problème

Cette réponse indique que l'utilisateur Google Play a déjà souscrit l'abonnement ou possède le produit à achat unique qu'il tente d'acheter. Dans la plupart des cas, il ne s'agit pas d'une erreur temporaire, sauf lorsqu'elle est causée par un cache Google Play obsolète.

Solution possible

Pour éviter que cette erreur ne se produise (autrement qu'à cause d'un problème de cache), ne proposez pas un produit à l'achat lorsque l'utilisateur le possède déjà. Assurez-vous de vérifier les droits d'accès de l'utilisateur lorsque vous affichez les produits disponibles à l'achat, et filtrez ce que l'utilisateur peut acheter en conséquence. Lorsque l'application cliente reçoit cette erreur en raison d'un problème de cache, l'erreur déclenche la mise à jour du cache Google Play avec les dernières données du backend de Play. Dans ce cas, effectuer une nouvelle tentative devrait résoudre cette erreur temporaire. Appelez BillingClient.queryPurchasesAsync() après avoir reçu une erreur ITEM_ALREADY_OWNED pour vérifier si l'utilisateur a acheté le produit. Si ce n'est pas le cas, implémentez une stratégie simple pour que l'utilisateur puisse réessayer d'effectuer l'achat.

ITEM_NOT_OWNED

Problème

Cette réponse d'achat indique que l'utilisateur Google Play n'a pas souscrit l'abonnement ou ne possède pas le produit à achat unique dont il tente de confirmer l'achat, ou qu'il tente de remplacer ou d'utiliser. Dans la plupart des cas, il ne s'agit pas d'une erreur temporaire, sauf lorsqu'elle est causée par un cache Google Play devenu obsolète.

Solution possible

Lorsque l'erreur vient d'un problème de cache, elle déclenche la mise à jour du cache Google Play avec les dernières données du backend de Play. Dans ce cas, implémenter une stratégie de nouvelle tentative simple devrait résoudre cette erreur temporaire. Appelez BillingClient.queryPurchasesAsync() après avoir reçu une erreur ITEM_NOT_OWNED pour vérifier si l'utilisateur a acheté le produit. Si ce n'est pas le cas, implémentez une stratégie simple pour que l'utilisateur puisse réessayer d'effectuer l'achat.

Réponses BillingResult non récupérables

Vous ne pouvez pas effectuer de récupération à partir de ces erreurs en utilisant une stratégie de nouvelle tentative.

FEATURE_NOT_SUPPORTED

Problème

Cette erreur non récupérable indique que la fonctionnalité Google Play Billing n'est pas prise en charge par l'appareil de l'utilisateur, probablement en raison d'une ancienne version du Play Store.

Par exemple, il est possible que certains appareils de vos utilisateurs ne prennent pas en charge la messagerie dans l'application.

Atténuation possible

Utilisez BillingClient.isFeatureSupported() pour vérifier la compatibilité des fonctionnalités avant d'appeler la bibliothèque Play Billing.

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

USER_CANCELED

Problème

L'utilisateur a cliqué en dehors de l'UI du processus de facturation.

Solution possible

Cette étape n'a qu'un but informatif et peut échouer sans complications.

ITEM_UNAVAILABLE

Problème

Le produit à achat unique ou l'abonnement Google Play Billing n'est pas disponible à l'achat pour cet utilisateur.

Atténuation possible

Assurez-vous que votre application actualise les informations détaillées sur le produit via queryProductDetailsAsync, tel que recommandé. Tenez compte de la fréquence à laquelle votre catalogue de produits est modifié sur la page de configuration de la Play Console pour implémenter des actualisations supplémentaires, si nécessaire. N'essayez de vendre que des produits sur Google Play Billing qui renvoient les bonnes informations via queryProductDetailsAsync. Vérifiez que la configuration de l'éligibilité du produit ne présente aucune incohérence. Par exemple, vous pouvez interroger un produit qui n'est disponible que pour une région autre que celle où l'utilisateur tente d'effectuer un achat. Pour être disponible à l'achat, un produit doit être actif, son application doit être publiée et disponible dans le pays de l'utilisateur.

Dans certains cas (en particulier lors des tests), tout est correct dans la configuration du produit, mais les utilisateurs continuent de voir cette erreur. Cela peut être dû à un délai de propagation des informations détaillées sur les produits qui survient sur les serveurs de Google. Réessayez plus tard.

DEVELOPER_ERROR

Problème

Il s'agit d'une erreur fatale qui indique que vous utilisez une API de manière inappropriée. Par exemple, si vous fournissez des paramètres incorrects à BillingClient.launchBillingFlow, cette erreur peut se produire.

Solution possible

Assurez-vous d'utiliser correctement les différents appels de la bibliothèque Play Billing. Consultez également le message de débogage pour en savoir plus sur l'erreur.