Обработка кодов ответа BillingResult

Когда вызов библиотеки биллинга Play запускает действие, библиотека возвращает ответ BillingResult чтобы проинформировать разработчиков о результате. Например, если вы используете queryProductDetailsAsync для получения доступных для пользователя предложений, код ответа либо содержит код ОК и предоставляет правильный объект ProductDetails , либо содержит другой ответ, указывающий причину, по которой объект ProductDetails не может быть предоставлен. .

Не все коды ответов являются ошибками. Справочная страница BillingResponseCode содержит подробное описание каждого ответа, обсуждаемого в этом руководстве. Некоторые примеры кодов ответов, которые не указывают на ошибки:

  • BillingClient.BillingResponseCode.OK : действие, вызванное вызовом, было успешно завершено.
  • BillingClient.BillingResponseCode.USER_CANCELED : для действий, которые отображают пользователю потоки пользовательского интерфейса Play Store, этот ответ указывает, что пользователь вышел из этих потоков пользовательского интерфейса, не завершив процесс.

Если код ответа действительно указывает на ошибку, причина иногда связана с переходными условиями, и поэтому восстановление возможно. Если вызов метода библиотеки выставления счетов Play возвращает значение BillingResponseCode , указывающее на восстанавливаемое состояние, вам следует повторить вызов. В других случаях состояния не считаются временными, и поэтому повторная попытка не рекомендуется.

Временные ошибки требуют разных стратегий повторных попыток в зависимости от таких факторов, как, например, происходит ли ошибка, когда пользователи находятся в сеансе (например, когда пользователь совершает процесс покупки), или ошибка возникает в фоновом режиме (например, когда вы выполняете запрос). существующие покупки пользователя во время onResume . В разделе «Стратегии повторных попыток» ниже приведены примеры этих различных стратегий, а в разделе «Повторные ответы BillingResult рекомендуется, какая стратегия лучше всего работает для каждого кода ответа.

Помимо кода ответа, некоторые ответы об ошибках включают сообщения для целей отладки и регистрации.

Стратегии повторных попыток

Простая повторная попытка

В ситуациях, когда пользователь находится в сеансе, лучше реализовать простую стратегию повторных попыток, чтобы ошибка как можно меньше мешала работе пользователя. В этом случае мы рекомендуем простую стратегию повторных попыток с максимальным количеством попыток в качестве условия выхода.

В следующем примере демонстрируется простая стратегия повторной попытки для обработки ошибки при установке соединения 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)
  }
  ...
}

Экспоненциальная повторная попытка отсрочки

Мы рекомендуем использовать экспоненциальную отсрочку для операций библиотеки Play Billing, которые происходят в фоновом режиме и не влияют на работу пользователя во время сеанса.

Например, было бы целесообразно реализовать это при подтверждении новых покупок, поскольку эта операция может выполняться в фоновом режиме, а подтверждение не обязательно должно происходить в реальном времени в случае возникновения ошибки.

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
}

Повторяемые ответы BillingResult

NETWORK_ERROR (код ошибки 12)

Проблема

Эта ошибка указывает на то, что возникла проблема с сетевым соединением между устройством и системами Play.

Возможное решение

Для восстановления используйте простые повторные попытки или экспоненциальную задержку, в зависимости от того, какое действие вызвало ошибку.

SERVICE_TIMEOUT (код ошибки -3)

Проблема

Эта ошибка означает, что запрос достиг максимального времени ожидания, прежде чем Google Play сможет ответить. Это может быть вызвано, например, задержкой выполнения действия, запрошенного вызовом библиотеки биллинга Play.

Возможное решение

Обычно это временная проблема. Повторите запрос, используя либо простую, либо экспоненциальную стратегию отсрочки, в зависимости от того, какое действие вернуло ошибку.

В отличие от SERVICE_DISCONNECTED приведенного ниже, соединение со службой выставления счетов Google Play не разрывается, и вам нужно только повторить любую попытку операции с библиотекой выставления счетов Play.

SERVICE_DISCONNECTED (код ошибки -1)

Проблема

Эта фатальная ошибка указывает на то, что соединение клиентского приложения со службой Google Play Store через BillingClient было разорвано.

Возможное решение

Чтобы максимально избежать этой ошибки, всегда проверяйте подключение к сервисам Google Play перед совершением вызовов с помощью библиотеки биллинга Play, вызывая BillingClient.isReady() .

Чтобы попытаться восстановить соединение из SERVICE_DISCONNECTED , ваше клиентское приложение должно попытаться восстановить соединение с помощью BillingClient.startConnection .

Как и в случае с SERVICE_TIMEOUT , используйте простые повторы или экспоненциальную отсрочку, в зависимости от того, какое действие вызвало ошибку.

SERVICE_UNAVAILABLE (код ошибки 2)

Важное примечание:

Начиная с библиотеки Google Play Billing 6.0.0, SERVICE_UNAVAILABLE больше не возвращается при проблемах с сетью. Он возвращается, когда служба выставления счетов недоступна и в устаревших сценариях SERVICE_TIMEOUT .

Проблема

Эта временная ошибка указывает на то, что служба выставления счетов Google Play в настоящее время недоступна. В большинстве случаев это означает, что существует проблема с сетевым подключением между клиентским устройством и службами выставления счетов Google Play.

Возможное решение

Обычно это временная проблема. Повторите запрос, используя либо простую, либо экспоненциальную стратегию отсрочки, в зависимости от того, какое действие вернуло ошибку.

В отличие от SERVICE_DISCONNECTED , соединение со службой выставления счетов Google Play не разрывается, и вам необходимо повторить любую попытку выполнения операции.

BILLING_UNAVAILABLE (код ошибки 3)

Проблема

Эта ошибка указывает на то, что в процессе покупки произошла ошибка выставления счетов пользователю. Примеры того, когда это может произойти, включают в себя:

  • Приложение Play Store на устройстве пользователя устарело.
  • Пользователь находится в неподдерживаемой стране.
  • Пользователь является корпоративным пользователем, и его администратор предприятия запретил пользователям совершать покупки.
  • Google Play не может взимать плату с выбранного пользователем способа оплаты. Например, срок действия кредитной карты пользователя мог быть истек.

Возможное решение

Автоматические повторные попытки вряд ли помогут в этом случае. Однако повторная попытка вручную может помочь, если пользователь устранит причину, вызвавшую проблему. Например, если пользователь обновит свою версию Play Store до поддерживаемой версии, может сработать повторение исходной операции вручную.

Если эта ошибка возникает, когда пользователь не находится в сеансе, повторная попытка может не иметь смысла. Когда вы получаете сообщение об ошибке BILLING_UNAVAILABLE в результате покупки, весьма вероятно, что пользователь получил отзыв от Google Play во время процесса покупки и может знать, что пошло не так. В этом случае вы можете показать сообщение об ошибке, указывающее, что что-то пошло не так, и предложить кнопку «Попробовать еще раз», чтобы дать пользователю возможность повторить попытку вручную после устранения проблемы.

ОШИБКА (код ошибки 6)

Проблема

Это фатальная ошибка, указывающая на внутреннюю проблему самого Google Play.

Возможное решение

Иногда внутренние проблемы Google Play, которые приводят к ERROR , носят временный характер, и для их устранения можно реализовать повторную попытку с экспоненциальной задержкой. Когда пользователи находятся в сеансе, предпочтительна простая повторная попытка.

ITEM_ALREADY_OWNED

Проблема

Этот ответ указывает на то, что у пользователя Google Play уже есть подписка или продукт, приобретаемый единоразово, который он пытается приобрести. В большинстве случаев это не временная ошибка, за исключением случаев, когда она вызвана устаревшим кешем Google Play.

Возможное решение

Чтобы избежать возникновения этой ошибки, если ее причиной не является проблема с кэшем, не предлагайте продукт для покупки, если он уже есть у пользователя. Обязательно проверяйте права пользователя, когда показываете продукты, доступные для покупки, и соответствующим образом фильтруйте то, что пользователь может приобрести. Когда клиентское приложение получает эту ошибку из-за проблемы с кешем, эта ошибка приводит к обновлению кеша Google Play последними данными из серверной части Play. В этом случае повторная попытка после ошибки должна разрешить этот конкретный временный экземпляр. Вызовите BillingClient.queryPurchasesAsync() после получения ITEM_ALREADY_OWNED , чтобы проверить, приобрел ли пользователь продукт, и если это не так, реализуйте простую логику повторной попытки для повторной попытки покупки.

ITEM_NOT_OWNED

Проблема

Этот ответ о покупке указывает на то, что пользователь Google Play не является владельцем подписки или продукта, приобретаемого единоразово, который пользователь пытается заменить, подтвердить или использовать. В большинстве случаев это не временная ошибка, за исключением случаев, когда она вызвана переходом кеша Google Play в устаревшее состояние.

Возможное решение

Когда ошибка возникает из-за проблемы с кешем, она заставляет кеш Google Play обновляться последними данными из серверной части Play. Повторная попытка с использованием простой стратегии повторения после ошибки должна разрешить этот конкретный временный экземпляр. Вызовите BillingClient.queryPurchasesAsync() после получения ITEM_NOT_OWNED , чтобы проверить, приобрел ли пользователь продукт. Если этого не произошло, используйте простую логику повторной попытки, чтобы повторить покупку.

Неповторяемые ответы BillingResult

Вы не можете исправить эти ошибки, используя логику повтора.

FEATURE_NOT_SUPPORTED

Проблема

Эта невосстанавливаемая ошибка указывает на то, что функция выставления счетов в Google Play не поддерживается на устройстве пользователя, вероятно, из-за старой версии Play Store.

Например, возможно, некоторые устройства ваших пользователей не поддерживают обмен сообщениями в приложении.

Возможное смягчение последствий

Используйте BillingClient.isFeatureSupported() , чтобы проверить поддержку функций перед вызовом библиотеки выставления счетов Play.

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

USER_CANCELED

Проблема

Пользователь вышел из пользовательского интерфейса потока выставления счетов.

Возможное решение

Это носит исключительно информационный характер и может привести к сбою.

ITEM_UNAVAILABLE

Проблема

Для этого пользователя недоступна подписка на Google Play Billing или продукт, приобретаемый единоразово.

Возможное смягчение последствий

Убедитесь, что ваше приложение обновляет сведения о продукте с помощью queryProductDetailsAsync , как рекомендовано. Учитывайте, как часто меняется ваш каталог продуктов в конфигурации Play Console, чтобы при необходимости выполнить дополнительные обновления. Попытайтесь продавать в Google Play только те продукты, которые возвращают правильную информацию через queryProductDetailsAsync . Проверьте конфигурацию приемлемости продукта на наличие несоответствий. Например, вы можете запрашивать продукт, который доступен только для региона, отличного от того, который пользователь пытается приобрести. Чтобы быть доступным для покупки, продукт должен быть активным, его приложение должно быть опубликовано, а его приложение должно быть доступно в стране пользователя.

Иногда, в частности во время тестирования, в конфигурации продукта все правильно, но пользователи все равно видят эту ошибку. Это может быть связано с задержкой распространения сведений о продукте на серверах Google. Повторите попытку позже.

DEVELOPER_ERROR

Проблема

Это фатальная ошибка, указывающая на неправильное использование API. Например, указание неверных параметров в BillingClient.launchBillingFlow может вызвать эту ошибку.

Возможное решение

Убедитесь, что вы правильно используете различные вызовы библиотеки Play Billing. Кроме того, проверьте сообщение отладки для получения дополнительной информации об ошибке.