Gestire i codici di risposta BillingResults

Quando una chiamata alla 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 ciascuna delle risposte discusse in questa guida. Alcuni esempi di codici di risposta che non indicano errori sono:

Quando il codice di risposta indica un errore, a volte la causa è dovuta a condizioni temporanee, quindi il recupero è possibile. Quando una chiamata a un metodo della libreria Play Billing restituisce un valore BillingResponseCode che indica una condizione recuperabile, devi riprovare la chiamata. In altri casi, le condizioni non sono considerate temporanee e pertanto non è consigliabile riprovare.

Gli errori temporanei richiedono strategie di ripetizione diverse a seconda di fattori quali se l'errore si verifica quando gli utenti sono in sessione, ad esempio quando un utente sta eseguendo un flusso di acquisto, o se l'errore si verifica in background, ad esempio quando esegui una query sugli acquisti esistenti dell'utente durante onResume. La sezione Strategie di ripetizione di seguito fornisce esempi di queste diverse strategie e la sezione Risposte ripetibili BillingResult consiglia la strategia più adatta per ogni codice di risposta.

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

Strategie di ripetizione dei tentativi

Nuovo tentativo semplice

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

L'esempio seguente mostra una semplice strategia di ripetizione per gestire un errore durante la creazione di una connessione 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) }
      } finally {
        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 vengono eseguite in background e non influiscono sull'esperienza utente durante la sessione.

Ad esempio, è opportuno implementare questa operazione quando si riconoscono nuovi acquisti, perché può avvenire in background e il riconoscimento 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 ripetibili

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 di Google Play.

Possibile risoluzione

Per il recupero, utilizza semplici tentativi o il 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 della Libreria Fatturazione Play.

Possibile risoluzione

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

A differenza di SERVICE_DISCONNECTED di seguito, la connessione al servizio di Fatturazione Google Play non viene interrotta e devi solo riprovare l'operazione della Libreria Fatturazione Play che è stata tentata.

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

La versione 8.0.0 della Libreria Fatturazione Play ha introdotto la funzionalità enableAutoServiceReconnection(). Ti consigliamo vivamente di attivare questa funzionalità durante la creazione del tuo BillingClient. Ciò consente alla libreria di tentare automaticamente di ristabilire la connessione quando viene effettuata una chiamata all'API di fatturazione mentre il servizio è disconnesso, riducendo significativamente il numero di occorrenze di questo errore.

Kotlin

val billingClient = BillingClient.newBuilder(context)
    .setListener(listener)
    .enablePendingPurchases()
    .enableAutoServiceReconnection() // Enable automatic service reconnection
    .build()

Java

BillingClient billingClient = BillingClient.newBuilder(context)
    .setListener(listener)
    .enablePendingPurchases()
    .enableAutoServiceReconnection() // Enable automatic service reconnection
    .build();
Se hai attivato la riconnessione automatica del servizio

La Libreria Fatturazione Play tenterà automaticamente di ristabilire la connessione. Se continui a ricevere un codice di risposta SERVICE_DISCONNECTED quando effettui una chiamata API, significa che la libreria non è riuscita a riconnettersi dopo i tentativi automatici. In questo scenario, devi implementare la logica di ripetizione nell'app:

  • Per le azioni avviate dall'utente (durante la sessione): utilizza semplici tentativi di ripetizione della chiamata API. Il problema di fondo potrebbe essere temporaneo.
  • Per le richieste in background:implementa i tentativi con backoff esponenziale per evitare di sovraccaricare il sistema se la disconnessione è prolungata.
Se NON hai attivato la riconnessione automatica del servizio

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

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

Come per SERVICE_TIMEOUT, utilizza semplici tentativi o backoff esponenziale, a seconda dell'azione che ha attivato l'errore.

SERVICE_UNAVAILABLE (codice di errore 2)

Nota importante:

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

Problema

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

Possibile risoluzione

In genere si tratta di un problema temporaneo. Riprova a inviare la richiesta utilizzando una strategia di backoff semplice o backoff 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 l'operazione che stai tentando.

BILLING_UNAVAILABLE (codice di errore 3)

Problema

Questo errore indica che si è verificato un errore di fatturazione utente durante il processo di acquisto. Ecco alcuni esempi di quando 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 la possibilità di effettuare acquisti.
  • Google Play non è in grado di addebitare l'importo sul metodo di pagamento dell'utente. Ad esempio, la carta di credito dell'utente potrebbe essere scaduta.
  • L'app Play Store è bloccata dal sistema (ad esempio, in una modalità Bambini personalizzata dall'OEM). In questo caso, il BillingResult include il messaggio di debug Play Store is blocked.

Possibile risoluzione

  • I tentativi automatici non sono 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, un nuovo tentativo manuale dell'operazione iniziale potrebbe funzionare.

  • Se questo errore si verifica quando l'utente non è in sessione, il nuovo tentativo potrebbe non avere senso. Quando ricevi un errore BILLING_UNAVAILABLE a seguito del flusso di acquisto, è molto probabile che l'utente abbia ricevuto feedback da Google Play durante la procedura di acquisto e potrebbe essere consapevole di cosa è andato storto. In questo caso, potresti mostrare un messaggio di errore che specifica che si è verificato un problema e offrire un pulsante Riprova per dare all'utente la possibilità di riprovare manualmente dopo aver risolto il problema.

ERRORE (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 e per la mitigazione può essere implementato un nuovo tentativo con backoff esponenziale. Quando gli utenti sono in sessione, è preferibile un semplice nuovo tentativo.

ITEM_ALREADY_OWNED

Problema

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

Possibile risoluzione

Per evitare che questo errore si verifichi quando la causa non è un problema di cache, non offrire un prodotto per l'acquisto quando l'utente lo possiede già. Assicurati di controllare i diritti dell'utente quando mostri i prodotti disponibili per l'acquisto e filtra di conseguenza ciò che l'utente può acquistare. 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 Play. Il nuovo tentativo dopo l'errore dovrebbe risolvere questa specifica istanza temporanea in questo caso. Chiama BillingClient.queryPurchasesAsync() dopo aver ricevuto un ITEM_ALREADY_OWNED per verificare se l'utente ha acquistato il prodotto e, in caso contrario, implementa una semplice logica di ripetizione per riprovare l'acquisto.

ITEM_NOT_OWNED

Problema

Questa risposta all'acquisto indica che l'utente di Google Play non possiede l'abbonamento o il prodotto con acquisto una tantum che sta tentando di sostituire, confermare o utilizzare. Nella maggior parte dei casi, questo non è un errore temporaneo, tranne quando è causato dalla cache di Google Play che diventa obsoleta.

Possibile risoluzione

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

Risposte BillingResult non recuperabili

Non puoi eseguire il recupero da questi errori utilizzando la logica di ripetizione.

FEATURE_NOT_SUPPORTED

Problema

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

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

Possibile mitigazione

Utilizza BillingClient.isFeatureSupported() per verificare il supporto delle funzionalità prima di effettuare la chiamata alla libreria Play Billing.

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

USER_CANCELED

Problema

L'utente ha chiuso l'interfaccia utente del flusso di fatturazione.

Possibile risoluzione

Queste informazioni sono solo a scopo informativo e possono essere ignorate.

ITEM_UNAVAILABLE

Problema

L'abbonamento o il prodotto acquistato una tantum di Fatturazione Google Play 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 cambia nella configurazione di Play Console per implementare aggiornamenti aggiuntivi, se necessario. Tenta di vendere solo i prodotti su Fatturazione Google Play che restituiscono le informazioni corrette tramite queryProductDetailsAsync. Controlla la configurazione dell'idoneità dei prodotti per eventuali incongruenze. Ad esempio, potresti eseguire una query per un prodotto disponibile solo per una regione diversa da quella in cui l'utente sta tentando di effettuare l'acquisto. Per essere acquistabile, un prodotto deve essere attivo e la relativa app deve essere pubblicata e disponibile nel paese dell'utente.

A volte, in particolare durante i test, la configurazione del prodotto è corretta, 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.

DEVELOPER_ERROR

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. Inoltre, controlla il messaggio di debug per ulteriori informazioni sull'errore.