Xử lý mã phản hồi BillingResult

Khi lệnh gọi trong Thư viện Play Billing kích hoạt một hành động, thư viện sẽ trả về phản hồi BillingResult để thông báo kết quả cho nhà phát triển. Ví dụ: nếu bạn sử dụng queryProductDetailsAsync để nhận các ưu đãi hiện có cho người dùng, mã phản hồi sẽ chứa mã OK và cung cấp đúng đối tượng ProductDetails hoặc mã phản hồi sẽ chứa một phản hồi khác cho biết lý do không thể cung cấp đối tượng ProductDetails.

Chỉ một số mã phản hồi là lỗi. Trang tham chiếu BillingResponseCode cung cấp nội dung mô tả chi tiết về từng phản hồi được thảo luận trong hướng dẫn này. Sau đây là một số ví dụ về mã phản hồi không chỉ ra lỗi:

Khi mã phản hồi chỉ ra lỗi, nguyên nhân đôi khi là do tình trạng tạm thời nên có thể khắc phục được. Khi lệnh gọi đến phương thức Thư viện Play Billing trả về giá trị BillingResponseCode cho biết tình trạng có thể khắc phục, bạn nên thử gọi lại. Trong các trường hợp khác, các tình trạng không được xem là tạm thời và do đó, bạn không nên thử lại.

Lỗi tạm thời đòi hỏi phải áp dụng các chiến lược thử lại khác nhau, tuỳ thuộc vào các yếu tố như lỗi có xảy ra khi người dùng đang hoạt động hay không (ví dụ: khi người dùng thực hiện quy trình mua) hoặc lỗi xảy ra trong chế độ nền — ví dụ: khi bạn truy vấn giao dịch mua hiện có của người dùng trong khi onResume. Phần chiến lược thử lại bên dưới đưa ra ví dụ về các chiến lược này và phần Phản hồi BillingResult có thể thử lại đề xuất chiến lược phù hợp nhất cho từng mã phản hồi.

Ngoài mã phản hồi, một số phản hồi lỗi còn có thông báo cho mục đích gỡ lỗi và ghi nhật ký.

Chiến lược thử lại

Thử lại theo cách đơn giản

Trong trường hợp người dùng đang hoạt động, tốt hơn hết là hãy triển khai một chiến lược thử lại đơn giản để hạn chế tối đa việc lỗi làm gián đoạn trải nghiệm người dùng. Trong trường hợp đó, bạn nên sử dụng chiến lược thử lại đơn giản với điều kiện thoát là số lần thử tối đa.

Ví dụ sau đây minh hoạ một chiến lược thử lại đơn giản để xử lý lỗi khi thiết lập kết nối 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)
  }
  ...
}

Thử lại bằng thuật toán thời gian đợi luỹ thừa

Bạn nên sử dụng thuật toán thời gian đợi luỹ thừa cho các thao tác với Thư viện Play Billing diễn ra ở chế độ nền và không ảnh hưởng đến trải nghiệm người dùng khi người dùng đang hoạt động.

Ví dụ: bạn nên triển khai thuật toán này khi xác nhận giao dịch mua mới vì thao tác này có thể diễn ra ở chế độ nền và không cần xác nhận theo thời gian thực nếu có lỗi.

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
}

Phản hồi BillingResult có thể thử lại

NETWORK_ERROR (Mã lỗi 12)

Sự cố

Lỗi này cho biết đã xảy ra sự cố kết nối mạng giữa thiết bị và hệ thống Play.

Giải pháp có thể áp dụng

Để khôi phục, hãy dùng các cách thử lại đơn giản hoặc thời gian đợi luỹ thừa, tuỳ theo thao tác đã gây ra lỗi.

SERVICE_TIMEOUT (Mã lỗi -3)

Sự cố

Lỗi này cho biết rằng yêu cầu đã hết thời gian chờ tối đa trước khi Google Play có thể phản hồi. Điều này có thể xảy ra, chẳng hạn như do sự chậm trễ trong quá trình thực thi hành động mà lệnh gọi trong Thư viện Play Billing yêu cầu.

Giải pháp có thể áp dụng

Đây thường là sự cố tạm thời. Thử yêu cầu lại bằng cách sử dụng chiến lược đơn giản hoặc thời gian đợi luỹ thừa, tuỳ vào hành động nào trả về lỗi.

Không giống như SERVICE_DISCONNECTED dưới đây, kết nối với Dịch vụ Google Play Billing vẫn duy trì và bạn chỉ cần thử lại thao tác mà mình đã cố gắng thực hiện với Thư viện Play Billing.

SERVICE_DISCONNECTED (Mã lỗi -1)

Sự cố

Lỗi nghiêm trọng này cho biết rằng kết nối của ứng dụng khách với dịch vụ Cửa hàng Google Play thông qua BillingClient đã bị ngắt.

Giải pháp có thể áp dụng

Để hạn chế tối đa lỗi này, hãy luôn kiểm tra kết nối với Dịch vụ Google Play trước khi thực hiện lệnh gọi với Thư viện Play Billing bằng cách gọi BillingClient.isReady().

Để thử khắc phục lỗi SERVICE_DISCONNECTED, ứng dụng khách của bạn nên cố gắng thiết lập lại kết nối bằng BillingClient.startConnection.

Tương tự như với SERVICE_TIMEOUT, hãy dùng cách thử lại đơn giản hoặc thời gian đợi luỹ thừa, tuỳ thuộc vào hành động nào kích hoạt lỗi.

SERVICE_UNAVAILABLE (Mã lỗi 2)

Lưu ý quan trọng:

Kể từ Thư viện Google Play Billing phiên bản 6.0.0, các sự cố mạng sẽ không trả về SERVICE_UNAVAILABLE nữa. Mã lỗi này được trả về khi dịch vụ thanh toán không có sẵn và các trường hợp SERVICE_TIMEOUT không còn được dùng nữa.

Sự cố

Lỗi tạm thời này cho biết dịch vụ Google Play Billing hiện không hoạt động. Trong hầu hết các trường hợp, điều này có nghĩa là xảy ra sự cố kết nối mạng ở bất cứ đâu giữa thiết bị khách và dịch vụ Google Play Billing.

Giải pháp có thể áp dụng

Đây thường là sự cố tạm thời. Thử yêu cầu lại bằng cách sử dụng chiến lược đơn giản hoặc thời gian đợi luỹ thừa, tuỳ vào hành động nào trả về lỗi.

Không giống như SERVICE_DISCONNECTED, kết nối với Dịch vụ Google Play Billing vẫn duy trì và bạn cần phải thử lại thao tác mà mình đang cố gắng thực hiện.

BILLING_UNAVAILABLE (Mã lỗi 3)

Sự cố

Lỗi này cho biết rằng đã xảy ra lỗi thanh toán trong quá trình mua. Ví dụ về trường hợp có thể xảy ra lỗi này:

  • Ứng dụng Cửa hàng Play trên thiết bị của người dùng đã lỗi thời.
  • Người dùng ở quốc gia không được hỗ trợ.
  • Người dùng là người dùng doanh nghiệp và quản trị viên doanh nghiệp của họ đã không cho phép người dùng mua hàng.
  • Google Play không thể tính phí vào phương thức thanh toán của người dùng. Ví dụ: thẻ tín dụng của người dùng có thể đã hết hạn.

Giải pháp có thể áp dụng

Bạn không thể thử lại theo cách tự động trong trường hợp này. Tuy nhiên, việc thử lại theo cách thủ công có thể giúp ích nếu người dùng giải quyết tình trạng gây ra sự cố. Ví dụ: nếu người dùng cập nhật phiên bản Cửa hàng Play lên một phiên bản được hỗ trợ, thì họ có thể thử lại thao tác ban đầu.

Nếu lỗi này xảy ra khi người dùng hiện không hoạt động, thì việc thử lại có thể không phù hợp. Nếu lỗi BILLING_UNAVAILABLE xảy ra do quy trình mua, rất có thể người dùng đã nhận được phản hồi của Google Play trong quá trình mua và có khả năng họ đã biết đó là lỗi gì. Trong trường hợp này, bạn có thể hiển thị thông báo lỗi cho biết đã xảy ra lỗi và cung cấp nút "Thử lại" để người dùng thử lại theo cách thủ công sau khi giải quyết vấn đề.

ERROR (Mã lỗi 6)

Sự cố

Lỗi nghiêm trọng này cho biết đã xảy ra sự cố nội bộ với chính Google Play.

Giải pháp có thể áp dụng

Đôi khi, các sự cố nội bộ với Google Play dẫn đến ERROR là tạm thời và bạn có thể thử lại bằng thuật toán thời gian đợi luỹ thừa để giảm thiểu tác động. Khi người dùng đang hoạt động, bạn nên thử lại theo cách đơn giản.

ITEM_ALREADY_OWNED

Sự cố

Phản hồi này cho biết rằng người dùng Google Play đã sở hữu gói thuê bao hoặc sản phẩm mua một lần mà họ đang muốn mua. Trong hầu hết các trường hợp, đây không phải là lỗi tạm thời, trừ phi lỗi này là do bộ nhớ đệm của Google Play đã lỗi thời.

Giải pháp có thể áp dụng

Để tránh xảy ra lỗi này khi nguyên nhân không phải là vấn đề về bộ nhớ đệm, không cung cấp sản phẩm để mua khi người dùng đã sở hữu sản phẩm đó. Hãy nhớ kiểm tra quyền của người dùng khi bạn hiển thị các sản phẩm có sẵn để mua và lọc ra những sản phẩm người dùng có thể mua tương ứng. Khi ứng dụng khách gặp lỗi này do vấn đề về bộ nhớ đệm, lỗi sẽ kích hoạt bộ nhớ đệm của Google Play để được cập nhật dữ liệu mới nhất từ phần phụ trợ của Play. Việc thử lại sau khi xảy ra lỗi sẽ giúp giải quyết vấn đề tạm thời cụ thể trong trường hợp này. Bạn có thể gọi BillingClient.queryPurchasesAsync() sau khi nhận được ITEM_ALREADY_OWNED để kiểm tra xem người dùng đã mua được sản phẩm hay chưa, nếu chưa thì hãy triển khai logic thử lại đơn giản để họ thử mua lại.

ITEM_NOT_OWNED

Sự cố

Phản hồi mua hàng này cho biết người dùng Google Play không sở hữu gói thuê bao hoặc sản phẩm mua một lần mà họ đang cố gắng thay thế, xác nhận hoặc tiêu thụ. Trong hầu hết các trường hợp, đây không phải là lỗi tạm thời, trừ phi lỗi này là do bộ nhớ đệm của Google Play đã lỗi thời.

Giải pháp có thể áp dụng

Khi xảy ra lỗi do vấn đề về bộ nhớ đệm, lỗi sẽ kích hoạt bộ nhớ đệm của Google Play để cập nhật dữ liệu mới nhất từ phần phụ trợ của Play. Việc thử lại bằng chiến lược thử lại đơn giản sau khi xảy ra lỗi sẽ giúp giải quyết vấn đề tạm thời cụ thể trong trường hợp này. Bạn có thể gọi BillingClient.queryPurchasesAsync() sau khi gặp lỗi ITEM_NOT_OWNED để kiểm tra xem người dùng đã mua được sản phẩm hay chưa. Nếu chưa thì hãy sử dụng logic thử lại đơn giản để họ thử mua lại.

Phản hồi BillingResult không thể thử lại

Bạn không thể khắc phục các lỗi này bằng logic thử lại.

FEATURE_NOT_SUPPORTED

Sự cố

Lỗi không thể thử lại này cho biết rằng tính năng Google Play Billing không được hỗ trợ trên thiết bị của người dùng, có thể là do phiên bản Cửa hàng Play đã cũ.

Ví dụ: có thể một số thiết bị của người dùng chưa hỗ trợ tính năng gửi thông báo trong ứng dụng.

Biện pháp khắc phục có thể áp dụng

Sử dụng BillingClient.isFeatureSupported() để kiểm tra việc hỗ trợ tính năng trước khi thực hiện lệnh gọi đến Thư viện Play Billing.

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

USER_CANCELED

Sự cố

Người dùng đã nhấp ra khỏi giao diện người dùng của luồng thanh toán.

Giải pháp có thể áp dụng

Nội dung này chỉ mang tính chất cung cấp thông tin và có thể không gây ảnh hưởng đến tổng thể.

ITEM_UNAVAILABLE

Sự cố

Người dùng này không thể mua gói thuê bao Google Play Billing hoặc sản phẩm mua một lần.

Biện pháp khắc phục có thể áp dụng

Đảm bảo ứng dụng của bạn làm mới thông tin chi tiết về sản phẩm thông qua queryProductDetailsAsync theo đề xuất. Lưu ý đến tần suất thay đổi danh sách sản phẩm trên cấu hình Play Console để tiến hành thêm các lần làm mới (nếu cần). Chỉ cố gắng bán những sản phẩm trên Google Play Billing trả về thông tin phù hợp thông qua queryProductDetailsAsync. Kiểm tra cấu hình về tính đủ điều kiện của sản phẩm để xem có sự không nhất quán nào không. Ví dụ: có thể bạn đang truy vấn một sản phẩm không có ở khu vực mà người dùng đang cố gắng mua. Một sản phẩm có sẵn để mua phải ở trạng thái hoạt động, ứng dụng của sản phẩm này phải được phát hành và đã có mặt tại quốc gia của người dùng.

Đôi khi, đặc biệt là trong quá trình kiểm thử, mọi thứ thuộc cấu hình sản phẩm đều chính xác, nhưng người dùng vẫn thấy lỗi này. Điều này có thể là do độ trễ lan truyền chi tiết sản phẩm trên các máy chủ của Google. Hãy thử lại sau.

DEVELOPER_ERROR

Sự cố

Lỗi nghiêm trọng này cho biết rằng bạn đang sử dụng API không đúng cách. Ví dụ: việc cung cấp tham số không chính xác cho BillingClient.launchBillingFlow có thể gây ra lỗi này.

Giải pháp có thể áp dụng

Hãy đảm bảo bạn đang sử dụng đúng cách các lệnh gọi trong Thư viện Play Billing. Ngoài ra, hãy kiểm tra thông báo gỡ lỗi để biết thêm thông tin về lỗi.