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

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

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

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

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

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

אנחנו ממליצים להשתמש בהשהיה מעריכית לפני ניסיון חוזר (exponential backoff) עבור פעולות בספריית החיובים ב-Play אשר מתרחשת ברקע ולא משפיעה על חוויית המשתמש בזמן שהמשתמש בפעילות.

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

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.

פתרון אפשרי

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

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

בעיה

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

פתרון אפשרי

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

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

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

בעיה

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

פתרון אפשרי

כדי למנוע את השגיאה הזו ככל האפשר, תמיד צריך לבדוק את החיבור ל-Google Play Services לפני שמבצעים קריאות ל-Play Billing Library, באמצעות קריאה ל-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 לגרסה נתמכת, יכול להיות שניסיון ידני חוזר של הפעולה הראשונית יצליח.

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

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

תגובות של תוצאות חיוב שלא ניתנות לאימות

אי אפשר להתאושש מהשגיאות האלה באמצעות לוגיקה של ניסיונות חוזרים.

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 או המוצר לרכישה חד-פעמית אינם זמין לרכישה עבור המשתמש הזה.

אפשרות מיטיגציה

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

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

developer_ERROR

בעיה

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

פתרון אפשרי

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