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