טיפול בקודי התגובה של Billing Results

כשקריאה לספריית החיובים ב-Play מפעילה פעולה, הספרייה מחזירה תגובה מסוג BillingResult כדי לעדכן את המפתחים לגבי התוצאה. לדוגמה, אם משתמשים ב-queryProductDetailsAsync כדי לקבל את המבצעים שזמינים למשתמש, קוד התגובה מכיל קוד OK ומספק את אובייקט ProductDetails הנכון, או שהוא מכיל תגובה אחרת שמציינת את הסיבה לכך שלא ניתן היה לספק את אובייקט ProductDetails.

לא כל קודי התגובה הם שגיאות. בדף ההפניה BillingResponseCode מופיע תיאור מפורט של כל אחת מהתגובות שמוזכרות במדריך הזה. דוגמאות לקודי תגובה שלא מציינים שגיאות:

  • BillingClient.BillingResponseCode.OK : הפעולה שהופעלה על ידי השיחה הושלמה בהצלחה.
  • BillingClient.BillingResponseCode.USER_CANCELED : עבור פעולות שמציגות למשתמש תהליכים בממשק המשתמש של חנות Play, התגובה הזו מציינת שהמשתמש עבר מתהליכי ממשק המשתמש האלה בלי להשלים את התהליך.

אם קוד התגובה מצביע על שגיאה, לפעמים הסיבה היא תנאים זמניים, ולכן אפשר לשחזר את הנתונים. כשקריאה ל-method של ספריית החיובים ב-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()
}
// ...

ניסיון חוזר עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff)

מומלץ להשתמש בהשהיה מעריכית לפני ניסיון חוזר (exponential backoff) לפעולות של ספריית החיובים ב-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.

פתרון אפשרי

כדי לשחזר, משתמשים בניסיונות חוזרים פשוטים או בהשהיה מעריכית לפני ניסיון חוזר (exponential backoff), בהתאם לפעולה שהפעילה את השגיאה.

SERVICE_TIMEOUT (קוד שגיאה -3)

בעיה

השגיאה הזו מציינת שהבקשה הגיעה לזמן הקצוב לתפוגה המקסימלי לפני ש-Google Play הצליחה להגיב. לדוגמה, יכול להיות שהעיכוב נובע מהשהיה בהרצת הפעולה שנדרשה על ידי הקריאה לספריית החיובים ב-Play.

פתרון אפשרי

בדרך כלל זו בעיה זמנית. מבצעים ניסיון חוזר של הבקשה באמצעות אסטרטגיית השהיה מעריכית לפני ניסיון חוזר (exponential backoff) או אסטרטגיה פשוטה, בהתאם לפעולה שהחזירה את השגיאה.

בניגוד לSERVICE_DISCONNECTED שבהמשך, החיבור לשירות החיובים ב-Google Play לא נותק, וצריך רק לנסות שוב את הפעולה בספריית החיובים ב-Play.

SERVICE_DISCONNECTED (קוד שגיאה ‎-1)

בעיה

השגיאה החמורה הזו מציינת שהחיבור של אפליקציית הלקוח לשירות Google Play Store דרך BillingClient נותק.

פתרון אפשרי

בגרסה 8.0.0 של ספריית החיובים ב-Play הושקה התכונה 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 תנסה להתחבר מחדש באופן אוטומטי. אם עדיין מקבלים קוד תגובה SERVICE_DISCONNECTED כשמבצעים קריאה ל-API, זה מצביע על כך שהספרייה לא הצליחה להתחבר מחדש אחרי הניסיונות האוטומטיים שלה. בתרחיש הזה, צריך להטמיע באפליקציה לוגיקה של ניסיון חוזר:

  • לפעולות שהמשתמש יזם (במהלך הסשן): משתמשים בניסיונות חוזרים פשוטים של קריאת ה-API. יכול להיות שהבעיה היא זמנית.
  • לבקשות ברקע: כדאי להטמיע ניסיונות חוזרים עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff) כדי למנוע עומס יתר על המערכת אם הניתוק נמשך זמן רב.
אם לא הפעלתם חיבור מחדש אוטומטי לשירות

כדי למנוע את השגיאה הזו ככל האפשר, חשוב תמיד לבדוק את החיבור לשירותי Google Play לפני שמבצעים קריאות באמצעות ספריית החיובים ב-Play. לשם כך, קוראים ל-BillingClient.isReady().

כדי לנסות לשחזר מ-SERVICE_DISCONNECTED, אפליקציית הלקוח צריכה לנסות ליצור מחדש את החיבור באמצעות BillingClient.startConnection.

בדומה ל-SERVICE_TIMEOUT, צריך להשתמש בניסיונות חוזרים פשוטים או בהשהיה מעריכית לפני ניסיון חוזר (exponential backoff), בהתאם לפעולה שהפעילה את השגיאה.

SERVICE_UNAVAILABLE (קוד שגיאה 2)

הערה חשובה:

החל מגרסה 6.0.0 של ספריית החיובים ב-Google Play, הערך SERVICE_UNAVAILABLE לא מוחזר יותר במקרה של בעיות ברשת. השגיאה הזו מוחזרת כשהשירות לחיוב לא זמין, ובתרחישי השימוש של SERVICE_TIMEOUT שהוצאו משימוש.

בעיה

השגיאה הזמנית הזו מציינת ששירות החיוב ב-Google Play לא זמין כרגע. ברוב המקרים, המשמעות היא שיש בעיה בחיבור לרשת בין מכשיר הלקוח לבין שירותי החיוב ב-Google Play.

פתרון אפשרי

בדרך כלל זו בעיה זמנית. מבצעים ניסיון חוזר של הבקשה באמצעות אסטרטגיית השהיה מעריכית לפני ניסיון חוזר (exponential backoff) או אסטרטגיה פשוטה, בהתאם לפעולה שהחזירה את השגיאה.

בניגוד ל-SERVICE_DISCONNECTED , החיבור לשירות חיוב ב-Google Play לא נותק, וצריך לנסות שוב לבצע את הפעולה.

BILLING_UNAVAILABLE (קוד שגיאה 3)

בעיה

השגיאה הזו מעידה על שגיאת חיוב של משתמש במהלך תהליך הרכישה. דוגמאות למקרים שבהם זה יכול לקרות:

  • האפליקציה של חנות Play במכשיר של המשתמש לא עדכנית.
  • המשתמש נמצא במדינה שלא נתמכת.
  • המשתמש הוא משתמש בגרסה ארגונית, והאדמין של הגרסה הארגונית השבית את האפשרות של המשתמשים לבצע רכישות.
  • ל-Google Play אין אפשרות לחייב את אמצעי התשלום של המשתמש. לדוגמה, יכול להיות שתוקף כרטיס האשראי של המשתמש פג.
  • המערכת חוסמת את אפליקציית חנות Play (לדוגמה, במצב ילדים שמותאם אישית על ידי יצרן ציוד מקורי). במקרה הזה, BillingResult כולל את הודעת הניפוי באגים חנות Play חסומה.

פתרון אפשרי

  • במקרה הזה, סביר להניח שניסיונות חוזרים אוטומטיים לא יעזרו. עם זאת, ניסיון חוזר ידני יכול לעזור אם המשתמש טיפל בתנאי שגרם לבעיה. לדוגמה, אם המשתמש מעדכן את הגרסה של חנות Play לגרסה נתמכת, יכול להיות שניסיון חוזר ידני של הפעולה הראשונית יצליח.

  • אם השגיאה הזו מתרחשת כשהמשתמש לא מחובר לסשן, יכול להיות שניסיון חוזר לא יעזור. אם אתם מקבלים שגיאה BILLING_UNAVAILABLE כתוצאה מתהליך הרכישה, סביר מאוד שהמשתמש קיבל משוב מ-Google Play במהלך תהליך הרכישה, ויכול להיות שהוא יודע מה השתבש. במקרה כזה, אפשר להציג הודעת שגיאה שמציינת שמשהו השתבש, ולהציע כפתור ניסיון חוזר כדי לתת למשתמש אפשרות לנסות שוב באופן ידני אחרי שהוא יטפל בבעיה.

ERROR (קוד שגיאה 6)

בעיה

זו שגיאה חמורה שמצביעה על בעיה פנימית ב-Google Play עצמו.

פתרון אפשרי

לפעמים יש בעיות פנימיות ב-Google Play שגורמות לשגיאה ERROR. במקרים כאלה, אפשר לנסות שוב עם השהיה אקספוננציאלית כדי לפתור את הבעיה. כשהמשתמשים נמצאים בסשן, עדיף לנסות שוב.

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.

לדוגמה, יכול להיות שמכשירים של חלק מהמשתמשים לא תומכים בהודעות בתוך האפליקציה.

אמצעי מניעה אפשריים

כדאי להשתמש ב-BillingClient.isFeatureSupported() כדי לבדוק את התמיכה בתכונות לפני שמתקשרים אל Play Billing Library.

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

USER_CANCELED

בעיה

המשתמש לחץ מחוץ לממשק המשתמש של רצף פעולות החיוב.

פתרון אפשרי

הפעולה הזו היא רק למטרות מידע, והיא יכולה להיכשל בצורה חלקה.

ITEM_UNAVAILABLE

בעיה

המינוי או המוצר ברכישה חד-פעמית דרך חיוב ב-Google Play לא זמינים לרכישה עבור המשתמש הזה.

אמצעי מניעה אפשריים

מומלץ לוודא שהאפליקציה מרעננת את פרטי המוצר באמצעות queryProductDetailsAsync. במהלך ההגדרה ב-Play Console, צריך לקחת בחשבון את התדירות שבה קטלוג המוצרים משתנה, כדי להטמיע רענונים נוספים אם צריך. נסו למכור מוצרים רק דרך מערכת החיוב ב-Google Play אם הם מחזירים את המידע הנכון דרך queryProductDetailsAsync. בודקים את הגדרת הזכאות של המוצר כדי לוודא שאין בה אי-התאמות. לדוגמה, יכול להיות שאתם מחפשים מוצר שזמין רק באזור אחר מזה שבו המשתמש מנסה לבצע רכישה. כדי שמוצר יהיה זמין לרכישה, הוא צריך להיות פעיל, האפליקציה שלו צריכה להיות גלויה לכולם והאפליקציה צריכה להיות זמינה במדינה של המשתמש.

לפעמים, במיוחד במהלך בדיקות, כל ההגדרות בתצורת המוצר תקינות, אבל המשתמשים עדיין רואים את השגיאה הזו. יכול להיות שהסיבה לכך היא עיכוב בהפצת פרטי המוצר בשרתים של Google. אפשר לנסות שוב מאוחר יותר.

DEVELOPER_ERROR

בעיה

זו שגיאה חמורה שמציינת שאתם משתמשים ב-API בצורה לא תקינה. לדוגמה, אם מעבירים פרמטרים שגויים אל BillingClient.launchBillingFlow, יכול להיות שתופיע השגיאה הזו.

פתרון אפשרי

חשוב לוודא שאתם משתמשים בצורה נכונה בקריאות השונות של ספריית החיובים ב-Play. כדאי גם לבדוק את הודעת הניפוי באגים כדי לקבל מידע נוסף על השגיאה.