Gestire i codici di risposta BillingResults

Quando una chiamata della Libreria Fatturazione Play attiva un'azione, la libreria restituisce una risposta BillingResult per informare gli sviluppatori del risultato. Ad esempio, se utilizzi queryProductDetailsAsync per ottenere le offerte disponibili per l'utente, il codice di risposta contiene un codice OK e fornisce l'oggetto ProductDetails corretto oppure contiene una risposta diversa che indica il motivo per cui non è stato possibile fornire l'oggetto ProductDetails.

Non tutti i codici di risposta sono errori. La pagina di riferimento BillingResponseCode fornisce una descrizione dettagliata di ogni risposta discussa in questa guida. Ecco alcuni esempi di codici di risposta che non indicano errori:

Quando il codice di risposta indica un errore, a volte la causa è dovuta a condizioni transitorie e pertanto è possibile il ripristino. Quando una chiamata a un metodo della Libreria Fatturazione Play restituisce un valore BillingResponseCode che indica una condizione recuperabile, devi riprovare a effettuare la chiamata. In altri casi, le condizioni non sono considerate temporanee e, di conseguenza, non è consigliabile effettuare un nuovo tentativo.

Gli errori temporanei richiedono diverse strategie per i nuovi tentativi in base a fattori quali, ad esempio, se l'errore si verifica quando gli utenti sono in sessione, ad esempio quando un utente segue un flusso di acquisto, o l'errore si verifica in background, ad esempio quando esegui query sugli acquisti esistenti dell'utente durante onResume. La sezione Strategie di nuovo tentativo di seguito fornisce esempi di queste diverse strategie, mentre la sezione Risposte BillingResult recuperabili consiglia la strategia più adatta per ciascun codice di risposta.

Oltre al codice di risposta, alcune risposte di errore includono messaggi per il debug e il logging.

Strategie per i nuovi tentativi

Nuovo tentativo semplice

Nelle situazioni in cui l'utente si trova in una sessione, è meglio implementare una semplice strategia di ripetizione in modo che l'errore interrompa il meno possibile l'esperienza utente. In questo caso, consigliamo una strategia semplice per i nuovi tentativi con un numero massimo di tentativi come condizione di uscita.

L'esempio seguente mostra una strategia semplice per i nuovi tentativi per gestire un errore quando viene stabilita una connessione a 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)
  }
  ...
}

Nuovo tentativo di backoff esponenziale

Ti consigliamo di utilizzare il backoff esponenziale per le operazioni della Libreria Fatturazione Play che avvengono in background e non influiscono sull'esperienza utente mentre l'utente è nella sessione.

Ad esempio, sarebbe appropriato implementare questa funzionalità quando si confermano i nuovi acquisti perché questa operazione può avvenire in background e la conferma non deve avvenire in tempo reale se si verifica un errore.

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
}

Risposte BillingResult recuperabili

NETWORK_ERROR (Codice di errore 12)

Problema

Questo errore indica che si è verificato un problema con la connessione di rete tra il dispositivo e i sistemi Google Play.

Possibile risoluzione

Per il ripristino, utilizza semplici tentativi o backoff esponenziale, a seconda dell'azione che ha attivato l'errore.

SERVICE_TIMEOUT (codice di errore -3)

Problema

Questo errore indica che la richiesta ha raggiunto il timeout massimo prima che Google Play possa rispondere. Ciò potrebbe essere causato, ad esempio, da un ritardo nell'esecuzione dell'azione richiesta dalla chiamata alla Libreria Fatturazione Play.

Possibile risoluzione

In genere si tratta di un problema temporaneo. Riprova la richiesta utilizzando una strategia di backoff semplice o esponenziale, a seconda dell'azione che ha restituito l'errore.

A differenza di SERVICE_DISCONNECTED riportato di seguito, la connessione al servizio di fatturazione Google Play non viene interrotta e devi soltanto ritentare qualsiasi operazione di Libreria Fatturazione Play effettuata.

SERVICE_DISCONNECTED (codice di errore -1)

Problema

Questo errore irreversibile indica che la connessione dell'app client al servizio Google Play Store tramite BillingClient è stata interrotta.

Possibile risoluzione

Per evitare il più possibile questo errore, controlla sempre la connessione a Google Play Services prima di effettuare chiamate con Libreria Fatturazione Play chiamando il numero BillingClient.isReady().

Per tentare il recupero da SERVICE_DISCONNECTED, l'app client deve provare a ristabilire la connessione utilizzando BillingClient.startConnection.

Proprio come con SERVICE_TIMEOUT, utilizza nuovi tentativi semplici o un backoff esponenziale, a seconda dell'azione che ha attivato l'errore.

SERVICE_UNAVAILABLE (codice di errore 2)

Nota importante:

A partire dalla versione 6.0.0 di Libreria Fatturazione Google Play, SERVICE_UNAVAILABLE non viene più restituito per problemi di rete. Viene restituito quando il servizio di fatturazione non è disponibile e gli scenari dei casi SERVICE_TIMEOUT deprecati.

Problema

Questo errore temporaneo indica che il servizio Fatturazione Google Play non è al momento disponibile. Nella maggior parte dei casi, questo significa che esiste un problema di connessione di rete tra il dispositivo client e i servizi di fatturazione di Google Play.

Possibile risoluzione

In genere si tratta di un problema temporaneo. Riprova la richiesta utilizzando una strategia di backoff semplice o esponenziale, a seconda dell'azione che ha restituito l'errore.

A differenza di SERVICE_DISCONNECTED, la connessione al servizio di fatturazione Google Play non viene interrotta e devi riprovare qualsiasi operazione tentata.

BILLING_UNAVAILABLE (Codice di errore 3)

Problema

Questo errore indica che si è verificato un errore di fatturazione dell'utente durante il processo di acquisto. Ecco alcuni esempi dei casi in cui ciò può verificarsi:

  • L'app Play Store sul dispositivo dell'utente non è aggiornata.
  • L'utente si trova in un paese non supportato.
  • L'utente è un utente aziendale e il suo amministratore aziendale ha disattivato l'accesso degli utenti agli acquisti.
  • Google Play non è in grado di effettuare addebiti sul metodo di pagamento dell'utente. Ad esempio, la carta di credito dell'utente potrebbe essere scaduta.

Possibile risoluzione

È improbabile che i nuovi tentativi automatici siano utili in questo caso. Tuttavia, un nuovo tentativo manuale può essere utile se l'utente risolve la condizione che ha causato il problema. Ad esempio, se l'utente aggiorna la versione del Play Store a una versione supportata, potrebbe funzionare un nuovo tentativo manuale dell'operazione iniziale.

Se questo errore si verifica quando l'utente non è nella sessione, riprovare potrebbe non avere senso. Quando ricevi un errore BILLING_UNAVAILABLE come conseguenza del flusso di acquisto, è molto probabile che l'utente abbia ricevuto un feedback da Google Play durante la procedura di acquisto e possa essere a conoscenza del problema. In questo caso, potresti mostrare un messaggio di errore che specifica che si è verificato un problema e offrire un pulsante "Riprova" per offrire all'utente la possibilità di riprovare manualmente dopo aver risolto il problema.

ERROR (Codice di errore 6)

Problema

Si tratta di un errore irreversibile che indica un problema interno di Google Play.

Possibile risoluzione

A volte i problemi interni di Google Play che portano a ERROR sono temporanei ed è possibile implementare un nuovo tentativo con un backoff esponenziale per la mitigazione. Quando gli utenti sono in sessione, è preferibile eseguire un semplice tentativo.

ELEMENTO_ALREADY_OWNED

Problema

Questa risposta indica che l'utente di Google Play possiede già l'abbonamento o il prodotto acquistato una tantum che sta tentando di acquistare. Nella maggior parte dei casi, non si tratta di un errore temporaneo, tranne quando è causato da una cache di Google Play inattiva.

Possibile risoluzione

Per evitare che questo errore si verifichi quando la causa non è un problema di cache, non offrire l'acquisto di un prodotto se è già di proprietà dell'utente. Assicurati di verificare i diritti dell'utente quando mostri i prodotti disponibili per l'acquisto e filtra i prodotti che l'utente può acquistare di conseguenza. Quando l'app client riceve questo errore a causa di un problema di cache, l'errore attiva l'aggiornamento della cache di Google Play con i dati più recenti del backend di Google Play. Riprovare dopo l'errore dovrebbe risolvere questa specifica istanza temporanea in questo caso. Chiama BillingClient.queryPurchasesAsync() dopo aver ottenuto un ITEM_ALREADY_OWNED per verificare se l'utente ha acquisito il prodotto e, in caso contrario, implementa una semplice logica di nuovo tentativo per ritentare l'acquisto.

ITEM_NOT_OWNED

Problema

Questa risposta di acquisto indica che l'utente di Google Play non possiede l'abbonamento o il prodotto di acquisto una tantum che l'utente sta tentando di sostituire, confermare o utilizzare. Nella maggior parte dei casi non si tratta di un errore temporaneo, tranne quando è causato dallo stato inattivo di Google Play.

Possibile risoluzione

Quando l'errore viene ricevuto a causa di un problema relativo alla cache, la cache di Google Play viene aggiornata con i dati più recenti del backend di Google Play. Un nuovo tentativo con una semplice strategia di ripetizione dopo l'errore dovrebbe risolvere questa specifica istanza temporanea. Chiama BillingClient.queryPurchasesAsync() dopo aver ricevuto un ITEM_NOT_OWNED per verificare se l'utente ha acquisito il prodotto. In caso contrario, utilizza la semplice logica di nuovo tentativo per ritentare l'acquisto.

Risposte BillingResult non ripetibili

Non puoi ripristinare questi errori utilizzando la logica per i nuovi tentativi.

FEATURE_NOT_SUPPORTED (FUNZIONALITÀ_NON_SUPPORTATA)

Problema

Questo errore non irreversibile indica che la funzionalità Fatturazione Google Play non è supportata sul dispositivo dell'utente, probabilmente a causa di una versione precedente del Play Store.

Ad esempio, forse alcuni dispositivi dei tuoi utenti non supportano la messaggistica in-app.

Possibile mitigazione

Utilizza BillingClient.isFeatureSupported() per verificare il supporto delle funzionalità prima di chiamare la Libreria Fatturazione Play.

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

UTENTE_ANNULLATO

Problema

L'utente ha fatto clic per uscire dall'interfaccia utente del flusso di fatturazione.

Possibile risoluzione

Si tratta di un'informazione puramente informativa e può non funzionare correttamente.

ITEM_UNAVAILABLE

Problema

L'abbonamento a Fatturazione Google Play o il prodotto acquistato una tantum non è disponibile per l'acquisto per questo utente.

Possibile mitigazione

Assicurati che l'app aggiorni i dettagli del prodotto tramite queryProductDetailsAsync come consigliato. Tieni conto della frequenza con cui il catalogo dei prodotti viene modificato nella configurazione di Play Console per implementare aggiornamenti aggiuntivi, se necessario. Cerca di vendere su Fatturazione Google Play soltanto prodotti che restituiscono le informazioni corrette tramite queryProductDetailsAsync. Verifica la presenza di incoerenze nella configurazione dell'idoneità del prodotto. Ad esempio, è possibile che tu stia eseguendo una query per un prodotto disponibile solo per un'area geografica diversa da quella che l'utente sta cercando di acquistare. Per essere acquistabile, un prodotto deve essere attivo, la relativa app deve essere pubblicata e la relativa app deve essere disponibile nel paese dell'utente.

A volte, in particolare durante i test, tutto è corretto nella configurazione del prodotto, ma gli utenti continuano a visualizzare questo errore. Ciò potrebbe essere dovuto a un ritardo di propagazione dei dettagli del prodotto sui server di Google. Riprova più tardi.

ERRORE_ SVILUPPATORE

Problema

Si tratta di un errore irreversibile che indica che stai utilizzando in modo improprio un'API. Ad esempio, fornire parametri errati a BillingClient.launchBillingFlow può causare questo errore.

Possibile risoluzione

Assicurati di utilizzare correttamente le diverse chiamate della Libreria Fatturazione Play. Controlla anche il messaggio di debug per ulteriori informazioni sull'errore.