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

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

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

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

Если код ответа указывает на ошибку, причина иногда кроется во временных условиях, и, следовательно, восстановление возможно. Если вызов метода библиотеки Play Billing возвращает значение 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) }
      } finally {
        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 Billing Library.

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

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

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

СЕРВИС ОТКЛЮЧЕН (Код ошибки -1)

Проблема

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

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

В версии 8.0.0 библиотеки Play Billing Library появилась функция enableAutoServiceReconnection() . Настоятельно рекомендуется включить эту функцию при создании вашего BillingClient . Это позволит библиотеке автоматически пытаться восстановить соединение при вызове API для выставления счетов, когда служба отключена, что значительно снизит вероятность возникновения этой ошибки.

Котлин

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();
Если у вас включено автоматическое переподключение к службе

Библиотека Play Billing автоматически попытается переподключиться. Если при вызове API вы по-прежнему получаете код ответа SERVICE_DISCONNECTED , это означает, что библиотеке не удалось переподключиться после автоматических попыток. В этом случае вам следует реализовать логику повторных попыток в вашем приложении:

  • Для действий, инициированных пользователем (в рамках сессии): используйте простые повторные попытки вызова API. Основная проблема может быть временной.
  • Для фоновых запросов: реализуйте повторные попытки с экспоненциальной задержкой, чтобы избежать перегрузки системы в случае длительного разрыва соединения.
Если вы НЕ включили автоматическое переподключение к службе

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

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

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

СЕРВИС НЕДОСТУПЕН (Код ошибки 2)

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

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

Проблема

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

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

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

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

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 , чтобы проверить, приобрел ли пользователь продукт. Если нет, используйте простую логику повтора для повторной попытки покупки.

Ответы Non-Retriable BillingResult

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

FEATURE_NOT_SUPPORTED

Проблема

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

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

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

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

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

ПОЛЬЗОВАТЕЛЬ_ОТМЕНЕН

Проблема

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

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

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

ТОВАР НЕДОСТУПЕН

Проблема

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

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

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

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

DEVELOPER_ERROR

Проблема

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

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

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