處理 BillingResult 回應代碼

當 Play 帳款服務程式庫的呼叫觸發動作時,程式庫會傳回 BillingResult 回應,通知開發人員結果。舉例來說,如果您使用 queryProductDetailsAsync 為使用者取得可用優惠,回應代碼會包含 OK 代碼,並提供合適的 ProductDetails 物件,或是包含不同回應,指出無法提供 ProductDetails 物件的原因。

並非所有回應代碼皆表示發生錯誤。BillingResponseCode 參考頁面針對本指南說明的各個回應提供詳細說明。以下列舉幾個並非發生錯誤的回應代碼示例:

如果回應代碼指出錯誤,有時原因可能是暫時性的,因此可以進行復原。若呼叫 Play 帳款服務程式庫方法時傳回 BillingResponseCode 值,表示此為可復原的狀況,您可以重試呼叫。在其他情況下,系統不會將錯誤視為暫時性的狀況,因此不建議重試。

暫時性錯誤會根據不同的因素採用不同的重試策略,例如錯誤發生時使用者是否處於工作階段狀態 (例如使用者正在進行購買流程),或錯誤是在背景發生 (例如,您在 onResume 期間查詢使用者的現有購買交易時)。下方的重試策略一節提供了這些不同策略的範例,可重試的 BillingResult 回應章節則提供建議,針對每個回應代碼選擇最適合的重試策略。

除了回應代碼之外,有些錯誤回應還包括用於偵錯及記錄的訊息。

重試策略

簡易重試

在使用者處於工作階段中時,您最好實作簡易的重試策略,盡可能避免干擾使用者體驗。在這種情況下,建議您採用簡易的重試策略,並以最大重試次數做為結束條件。

以下示例說明如何在建立 BillingClient 連線時,使用簡易的重試策略處理錯誤:

// Initialize the BillingClient.
private val billingClient = BillingClient.newBuilder(context)
    .setListener(this)
    .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
    .build()

private val coroutineScope = kotlinx.coroutines.CoroutineScope(
    kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.Main.immediate
)

private var connectionJob: kotlinx.coroutines.Job? = null

// Establish a connection to Google Play.
fun startBillingConnection() {
    connectionJob?.cancel()
    connectionJob = coroutineScope.launch {
        connectWithRetry()
    }
}

// Suspended helper to perform a single connection attempt
private suspend fun connectBilling(): BillingResult =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (continuation.isActive) {
                    continuation.resume(billingResult)
                }
            }

            override fun onBillingServiceDisconnected() {
                Log.e(TAG, "Google Play Billing Service disconnected")
                if (continuation.isActive) {
                    continuation.resume(
                        BillingResult.newBuilder()
                            .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
                            .setDebugMessage("Service disconnected during connection setup")
                            .build()
                    )
                } else {
                    startBillingConnection()
                }
            }
        })
    }

// Billing connection retry logic. This is a simple max retry pattern
private suspend fun connectWithRetry() {
    val maxTries = 3
    var tries = 1
    var isConnectionEstablished = false
    while (tries <= maxTries && !isConnectionEstablished) {
        val billingResult = connectBilling()
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            isConnectionEstablished = true
            Log.d(TAG, "Billing response OK")
        } else {
            Log.e(TAG, "Billing connection retry failed: ${billingResult.debugMessage}")
            tries++
            if (tries <= maxTries) {
                delay(2000L) // Wait 2 seconds before retrying
            }
        }
    }
}

fun cleanUp() {
    coroutineScope.cancel()
}
// ...

指數輪詢重試

建議您為 Play 帳款服務程式庫作業進行指數輪詢重試,因為這項策略是在背景執行,且在使用者處於工作階段時不會影響使用者體驗。

例如,在確認新購買交易時就很適合使用這項策略,因為它可以在背景執行,如果發生錯誤,也不需要即時確認。

private suspend fun acknowledge(purchaseToken: String): BillingResult =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        val params = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchaseToken)
            .build()
        billingClient.acknowledgePurchase(params) { billingResult ->
            continuation.resumeWith(Result.success(billingResult))
        }
    }

private suspend fun queryPurchases(productType: String): Pair<BillingResult, List<Purchase>> =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        val params = QueryPurchasesParams.newBuilder()
            .setProductType(productType)
            .build()
        billingClient.queryPurchasesAsync(params) { billingResult, purchaseList ->
            continuation.resumeWith(Result.success(Pair(billingResult, purchaseList)))
        }
    }

suspend fun acknowledgePurchase(purchaseToken: String) {
    val retryDelayMs = 2000L
    val retryFactor = 2
    val maxTries = 3

    var tries = 1
    var currentDelay = retryDelayMs
    var acknowledgePurchaseResult: BillingResult

    do {
        acknowledgePurchaseResult = acknowledge(purchaseToken)
        val playBillingResponseCode = acknowledgePurchaseResult.responseCode

        when (playBillingResponseCode) {
            BillingClient.BillingResponseCode.OK -> {
                Log.i(TAG, "Acknowledgement was successful")
                return
            }

            BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
                Log.d(TAG, "Acknowledgement failed with ITEM_NOT_OWNED")
                val (billingResult, purchaseList) = queryPurchases(BillingClient.ProductType.SUBS)
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    purchaseList.forEach { purchase ->
                        acknowledge(purchase.purchaseToken)
                    }
                }
                return
            }

            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}"
                )
                if (tries < maxTries) {
                    delay(currentDelay)
                    currentDelay *= retryFactor
                    tries++
                } else {
                    break
                }
            }

            else -> {
                Log.e(
                    TAG,
                    "Acknowledgement failed and cannot be retried -- " +
                        "Response Code: ${acknowledgePurchaseResult.responseCode} -- " +
                        "Debug Message: ${acknowledgePurchaseResult.debugMessage}"
                )
                throw Exception("Failed to acknowledge the purchase!")
            }
        }
    } while (tries <= maxTries)

    throw Exception("Failed to acknowledge the purchase after $maxTries attempts!")
}

可重試的 BillingResult 回應

NETWORK_ERROR (錯誤代碼 12)

問題

此錯誤表示裝置和 Play 系統之間的網路連線發生問題。

可能的解決方法

如要進行復原,請依據觸發錯誤的動作,判斷要採用簡易的重試方法或指數輪詢。

SERVICE_TIMEOUT (錯誤代碼 -3)

問題

這個錯誤表示在 Google Play 可回應之前,該要求已達到逾時上限。這可能是因 Play 帳款服務程式庫呼叫了動作要求,而執行動作延遲所導致。

可能的解決方法

這通常是暫時性問題。請依據傳回錯誤的動作,決定要採用簡易策略或指數輪詢策略來重試要求。

與下方的 SERVICE_DISCONNECTED 不同,與 Google Play 帳款服務的連線並未中斷,您只需重試先前嘗試執行的任何 Play 帳款服務程式庫作業。

SERVICE_DISCONNECTED (錯誤代碼 -1)

問題

此嚴重錯誤表示用戶端應用程式透過 BillingClient 連至 Google Play 商店服務的連線已中斷。

可能的解決方法

Play 帳款服務程式庫 8.0.0 版導入了enableAutoServiceReconnection()功能。強烈建議您在建構 BillingClient 時啟用這項功能。這樣一來,當服務中斷連線時,程式庫就能在發出帳單 API 呼叫時自動嘗試重新建立連線,大幅減少發生這類錯誤的次數。

Kotlin

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

Java

BillingClient billingClient = BillingClient.newBuilder(context)
    .setListener(listener)
    .enablePendingPurchases()
    .enableAutoServiceReconnection() // Enable automatic service reconnection
    .build();
如果已啟用自動重新連線服務

Play Billing Library 會自動嘗試重新連線。如果在發出 API 呼叫時仍收到 SERVICE_DISCONNECTED 回應代碼,表示程式庫在自動嘗試後仍無法重新連線。在這種情況下,您應在應用程式中實作重試邏輯:

  • 使用者啟動的動作 (工作階段內):簡單重試 API 呼叫。根本問題可能只是暫時性的。
  • 背景要求:如果連線中斷時間較長,請實作指數輪詢重試機制,避免系統負載過重。
如果尚未啟用自動重新連線服務

為了避免發生這個錯誤,請一律透過 BillingClient.isReady() 呼叫,先檢查與 Google Play 服務的連線,再使用 Play 帳款服務程式庫進行呼叫。

如要嘗試從 SERVICE_DISCONNECTED 復原,您的用戶端應用程式應使用 BillingClient.startConnection,嘗試重新建立連線。

SERVICE_TIMEOUT 一樣,請依據觸發錯誤的動作決定要採用簡易重試或指數輪詢重試策略。

SERVICE_UNAVAILABLE (錯誤代碼 2)

重要注意事項:

自 Google Play 帳款服務程式庫 6.0.0 版起,網路問題將不再傳回 SERVICE_UNAVAILABLE。在帳單服務無法使用和已淘汰 SERVICE_TIMEOUT 的情況下,系統會傳回這個錯誤。

問題

這個暫時性錯誤表示 Google Play 帳款服務服務目前無法使用。在大多數情況下,這表示用戶端裝置和 Google Play 帳款服務之間發生網路連線問題。

可能的解決方法

這通常是暫時性問題。請依據傳回錯誤的動作,決定要採用簡易策略或指數輪詢策略來重試要求。

有別於 SERVICE_DISCONNECTED,與 Google Play 帳款服務的連線並未中斷,您只需重試先前嘗試執行的任何作業。

BILLING_UNAVAILABLE (錯誤代碼 3)

問題

這個錯誤代表購買流程中發生使用者結帳錯誤。以下列出幾項可能原因:

  • 使用者裝置上的 Play 商店應用程式版本過舊。
  • 使用者位於不支援的國家/地區。
  • 使用者是企業使用者,且該企業的管理員已禁止使用者購買產品。
  • Google Play 無法依據使用者的付款方式收取款項。舉例來說,使用者的信用卡可能已過期。
  • 系統封鎖了 Play 商店應用程式 (例如在 OEM 客製化兒童模式中)。在這種情況下,BillingResult會包含「Play Store is blocked」(Play 商店已遭封鎖) 的偵錯訊息。

可能的解決方法

  • 在這種情況下,自動重試可能無法解決問題。不過,如果使用者能解決造成問題的原因,就能透過手動重試方式解決問題。舉例來說,如果使用者將 Play 商店更新至支援的版本,就能以手動重試方式執行初始作業。

  • 如果錯誤是在使用者非處於工作階段的期間發生,重試可能不太合理。如果您收到因購買流程而產生的 BILLING_UNAVAILABLE 錯誤,很可能是因為使用者在購買過程中收到來自 Google Play 的意見回饋,且可能知道問題為何。在這種情況下,您可以對使用者顯示錯誤訊息,指出系統發生錯誤,並提供「再試一次」按鈕,讓使用者在解決問題後可以手動重試。

ERROR (錯誤代碼 6)

問題

此為嚴重錯誤,表示 Google Play 本身發生了內部問題。

可能的解決方法

有時造成 ERROR 的 Google Play 內部問題是暫時性的,且您可以使用指數輪詢策略進行重試來緩解問題。使用者在工作階段中時,建議您進行簡易重試。

ITEM_ALREADY_OWNED

問題

此回應表示 Google Play 使用者已擁有他們要購買的訂閱項目或一次性消費產品。在大多數情況下,這並非暫時性錯誤。除非這項錯誤是過時的 Google Play 快取資料所造成。

可能的解決方法

為避免錯誤是由快取以外的問題所造成,請勿在使用者擁有某項產品時販售相同產品。顯示可供購買的產品時,請務必檢查使用者的授權,藉此篩選使用者可購買的產品。當用戶端應用程式因快取問題而收到這個錯誤時,就會觸發 Google Play 快取,從 Play 後端取得最新資料。在錯誤發生後進行重試,應該就能解決這個暫時性的情況。在收到 ITEM_ALREADY_OWNED 後呼叫 BillingClient.queryPurchasesAsync(),確認使用者是否已購買該產品。如果沒有,請實作簡易重試邏輯,重新嘗試購買。

ITEM_NOT_OWNED

問題

此購買回應表示 Google Play 使用者並未擁有想要取代、確認或使用的訂閱項目或一次性消費產品。在大多數情況下,這並非暫時性錯誤。除非這項錯誤是 Google Play 快取取得過時資料所導致。

可能的解決方法

如果錯誤是因快取問題而產生,則錯誤會觸發 Google Play 快取,讓系統使用 Play 後端的最新資料進行更新。在錯誤發生之後進行簡易重試策略,應該能解決這個特定的暫態錯誤。取得 ITEM_NOT_OWNED 後呼叫 BillingClient.queryPurchasesAsync(),檢查使用者是否已取得相應產品。如果沒有,請使用簡單的重試邏輯來重新嘗試購買。

無法重試的 BillingResult 回應

您無法使用重試邏輯從這些錯誤中復原。

FEATURE_NOT_SUPPORTED

問題

此無法重試的錯誤表示使用者的裝置不支援 Google Play 帳款服務功能,原因可能是 Play 商店版本較舊。

舉例來說,可能有部分使用者的裝置不支援應用程式內通訊功能。

可能的緩解措施

呼叫 Play 帳款服務程式庫之前,請使用 BillingClient.isFeatureSupported() 檢查功能支援情況。

when {
    billingClient.isReady -> {
        val billingResult =
            billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            // use Feature
        }
    }
}

USER_CANCELED

問題

使用者已退出結帳流程 UI。

可能的解決方法

這項資訊僅供參考,且可能會在不中斷的情況下失敗。

ITEM_UNAVAILABLE

問題

此使用者無法購買 Google Play 帳款服務訂閱或一次性消費產品。

可能的緩解措施

請務必讓您的應用程式按照建議,透過 queryProductDetailsAsync 更新產品詳細資料。請考量產品目錄在 Play 管理中心設定上的變更頻率,以便視需要執行額外更新。只嘗試在 Google Play 帳款服務上販售透過 queryProductDetailsAsync 傳回正確資訊的產品。檢查產品資格設定是否有任何不一致。舉例來說,您可能正在查詢某項產品,而該產品只在使用者嘗試購買的地區外販售。想在某個國家/地區販售應用程式內產品時,除了啟用產品外,您也必須在當地發布產品所屬的應用程式。

有時在特定測試期間,所有產品設定都正確無誤,但使用者仍會看到這個錯誤。原因可能是 Google 伺服器上的產品詳細資料傳播延遲。請稍後再試。

DEVELOPER_ERROR

問題

此為嚴重錯誤,表示您使用 API 的方式不當。舉例來說,向 BillingClient.launchBillingFlow 提供不正確的參數可能會發生這項錯誤。

可能的解決方法

請確認您正確使用各種 Play 帳款服務程式庫的呼叫。此外,請查看偵錯訊息,進一步瞭解錯誤內容。