כשקריאה לספריית החיובים ב-Play מפעילה פעולה, הספרייה מחזירה תגובה מסוג BillingResult כדי לעדכן את המפתחים לגבי התוצאה. לדוגמה, אם משתמשים ב-queryProductDetailsAsync כדי לקבל את המבצעים שזמינים למשתמש, קוד התגובה מכיל קוד OK ומספק את אובייקט ProductDetails הנכון, או שהוא מכיל תגובה אחרת שמציינת את הסיבה לכך שלא ניתן היה לספק את אובייקט ProductDetails.
לא כל קודי התגובה הם שגיאות. בדף ההפניה BillingResponseCode מופיע תיאור מפורט של כל אחת מהתגובות שמוזכרות במדריך הזה.
דוגמאות לקודי תגובה שלא מציינים שגיאות:
-
BillingClient.BillingResponseCode.OK: הפעולה שהופעלה על ידי השיחה הושלמה בהצלחה. -
BillingClient.BillingResponseCode.USER_CANCELED: עבור פעולות שמציגות למשתמש מסלולי ממשק משתמש בחנות Play, התגובה הזו מציינת שהמשתמש עבר ממסלולי ממשק המשתמש האלה בלי להשלים את התהליך.
אם קוד התגובה מציין שגיאה, לפעמים הסיבה היא תנאים זמניים, ולכן אפשר לשחזר את הנתונים. כשקריאה ל-method של ספריית החיובים ב-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) }
} finally {
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 נותק.
פתרון אפשרי
מומלץ מאוד: הפעלת חיבור מחדש אוטומטי לשירות
בגרסה 8.0.0 של ספריית החיובים ב-Play הושקה התכונה enableAutoServiceReconnection().
מומלץ מאוד להפעיל את התכונה הזו כשמפתחים את BillingClient. כך, הספרייה יכולה לנסות באופן אוטומטי ליצור מחדש את החיבור כשמתבצעת קריאה ל-API של החיוב בזמן שהשירות מנותק, מה שמפחית באופן משמעותי את המקרים שבהם השגיאה הזו מתרחשת.
Kotlin
val billingClient = BillingClient.newBuilder(context)
.setListener(listener)
.enablePendingPurchases()
.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 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 במהלך תהליך הרכישה, ויכול להיות שהוא יודע מה השתבש. במקרה כזה, אפשר להציג הודעת שגיאה שמציינת שמשהו השתבש, ולהציע כפתור 'ניסיון חוזר' כדי לתת למשתמש אפשרות לנסות שוב באופן ידני אחרי שהוא יטפל בבעיה.
ERROR (קוד שגיאה 6)
בעיה
זו שגיאה חמורה שמצביעה על בעיה פנימית ב-Google Play עצמו.
פתרון אפשרי
לפעמים בעיות פנימיות ב-Google Play שמובילות לERROR הן זמניות, ואפשר ליישם ניסיון חוזר עם השהיה מעריכית לפני ניסיון חוזר (exponential backoff) כדי לפתור אותן. כשהמשתמשים נמצאים בסשן, עדיף לנסות שוב.
ITEM_ALREADY_OWNED
בעיה
התגובה הזו מציינת שלמשתמש ב-Google Play כבר יש מינוי או מוצר לרכישה חד-פעמית שהוא מנסה לרכוש. ברוב המקרים, זו לא שגיאה חולפת, אלא אם היא נגרמת על ידי מטמון לא עדכני של Google Play.
פתרון אפשרי
כדי להימנע מהשגיאה הזו במקרים שבהם הבעיה לא קשורה למטמון, אל תציעו מוצר לרכישה אם הוא כבר בבעלות המשתמש. חשוב לבדוק את ההרשאות של המשתמש כשמציגים לו את המוצרים שזמינים לרכישה, ולסנן את המוצרים שהוא יכול לרכוש בהתאם.
כשאפליקציית הלקוח מקבלת את השגיאה הזו בגלל בעיה במטמון, השגיאה גורמת לעדכון המטמון של Google Play עם הנתונים העדכניים מהקצה העורפי של Play.
במקרה הזה, ניסיון חוזר אחרי השגיאה אמור לפתור את הבעיה הספציפית הזו. אחרי שמקבלים ITEM_ALREADY_OWNED, מתקשרים אל BillingClient.queryPurchasesAsync() כדי לבדוק אם המשתמש רכש את המוצר. אם לא, מטמיעים לוגיקה פשוטה של ניסיון חוזר כדי לנסות שוב לבצע את הרכישה.
ITEM_NOT_OWNED
בעיה
תגובת הרכישה הזו מציינת שלמשתמש ב-Google Play אין מינוי או מוצר ב<b>רכישה חד-פעמית</b> שהמשתמש מנסה להחליף, לאשר או להשתמש בו. ברוב המקרים זו לא שגיאה זמנית, אלא אם היא נגרמת כשהמטמון של Google Play מגיע למצב לא עדכני.
פתרון אפשרי
כשמתקבלת שגיאה בגלל בעיה במטמון, השגיאה גורמת לעדכון המטמון של Google Play עם הנתונים העדכניים ביותר מהקצה העורפי של Play. ניסיון חוזר עם אסטרטגיית ניסיון חוזר פשוטה אחרי השגיאה אמור לפתור את המקרה הספציפי הזה של שגיאה זמנית. אחרי שמקבלים ITEM_NOT_OWNED, קוראים לפונקציה BillingClient.queryPurchasesAsync() כדי לבדוק אם המשתמש רכש את המוצר. אם לא, צריך להשתמש בלוגיקה פשוטה של ניסיון חוזר כדי לנסות שוב לבצע את הרכישה.
תגובות Non-Retriable BillingResult
אי אפשר לפתור את השגיאות האלה באמצעות לוגיקה של ניסיון חוזר.
FEATURE_NOT_SUPPORTED
בעיה
השגיאה הזו לא ניתנת לשחזור, והיא מציינת שהתכונה 'חיוב ב-Google Play' לא נתמכת במכשיר של המשתמש, כנראה בגלל גרסה ישנה של חנות Play.
לדוגמה, יכול להיות שמכשירים של חלק מהמשתמשים לא תומכים בהודעות בתוך האפליקציה.
אמצעי אפשרי לצמצום הסיכון
כדאי להשתמש ב-BillingClient.isFeatureSupported() כדי לבדוק את התמיכה בתכונות לפני שמתקשרים אל Play Billing Library.
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. כדאי גם לבדוק את הודעת הניפוי באגים כדי לקבל מידע נוסף על השגיאה.