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

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

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

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

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

תגובות של תוצאות חיוב שאפשר לשחזר

NETWORK_ERROR (קוד שגיאה 12)

בעיה

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

פתרון אפשרי

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

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

בעיה

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

פתרון אפשרי

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

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

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

בעיה

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

פתרון אפשרי

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

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

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

בעיה

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

פתרון אפשרי

לפעמים בעיות פנימיות של Google Play שמובילות ל-ERROR זמניים, וניתן ליישם ניסיון חוזר עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff) צמצום ההשפעות. כשהמשתמשים מחוברים לסשן, עדיף לבצע ניסיון חוזר פשוט.

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

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

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

FEATURE_NOT_SUPPORTED

בעיה

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

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

צמצום אפשרי

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