عندما يؤدي طلب من Play Billing Library إلى إجراء، تعرض المكتبة استجابة
BillingResult
لإعلام المطوّرين بالنتيجة. على سبيل المثال، إذا كنت تستخدم
queryProductDetailsAsync
للحصول على العروض المتاحة للمستخدم، يحتوي رمز الاستجابة إما على رمز
OK ويقدّم عنصر 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) }
} finally {
tries++
}
} while (tries <= maxTries && !isConnectionEstablished)
}
...
}
إعادة المحاولة باستخدام خوارزمية الرقود الأسي الثنائي
ننصحك باستخدام خوارزمية الرقود الأسي الثنائي لعمليات Play Billing Library التي تحدث في الخلفية ولا تؤثر في تجربة المستخدم أثناء وجوده في جلسة.
على سبيل المثال، من المناسب تنفيذ هذه الاستراتيجية عند تأكيد عمليات الشراء الجديدة لأنّ هذه العملية يمكن أن تحدث في الخلفية، ولا يلزم إجراء التأكيد في الوقت الفعلي إذا حدث خطأ.
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.
الحلّ المحتمَل
لحلّ المشكلة، استخدِم عمليات إعادة المحاولة البسيطة أو خوارزمية الرقود الأسي الثنائي، استنادًا إلى الإجراء الذي أدّى إلى حدوث الخطأ.
SERVICE_TIMEOUT (رمز الخطأ -3)
المشكلة
يشير هذا الخطأ إلى أنّ الطلب قد وصل إلى الحدّ الأقصى للمهلة قبل أن يتمكّن Google Play من الردّ. يمكن أن يحدث ذلك، على سبيل المثال، بسبب تأخير في تنفيذ الإجراء الذي طلبه طلب Play Billing Library.
الحلّ المحتمَل
عادةً ما تكون هذه المشكلة مؤقتة. أعِد محاولة الطلب باستخدام استراتيجية إعادة المحاولة البسيطة أو خوارزمية الرقود الأسي الثنائي، استنادًا إلى الإجراء الذي عرض الخطأ.
على عكس SERVICE_DISCONNECTED
أدناه، لا يتم قطع الاتصال بخدمة الفوترة في Google Play، وما عليك سوى إعادة محاولة أي عملية من عمليات Play Billing Library التي تمت محاولتها.
SERVICE_DISCONNECTED (رمز الخطأ -1)
المشكلة
يشير هذا الخطأ الجسيم إلى أنّ اتصال تطبيق العميل بخدمة Google Play
Store من خلال BillingClient
قد تم قطعه.
الحلّ المحتمَل
ننصحك بشدة بتفعيل ميزة إعادة الاتصال التلقائي بالخدمة
قدّم الإصدار 8.0.0 من Play Billing Library ميزة enableAutoServiceReconnection().
ننصحك بشدة بتفعيل هذه الميزة عند إنشاء BillingClient. يسمح ذلك للمكتبة بمحاولة إعادة إنشاء الاتصال تلقائيًا عند إجراء طلب بيانات من واجهة برمجة التطبيقات للفوترة أثناء قطع الاتصال بالخدمة، ما يقلّل بشكل كبير من حالات ورود هذا الخطأ.
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 Billing Library إعادة الاتصال تلقائيًا. إذا استمر ظهور رمز الاستجابة SERVICE_DISCONNECTED عند إجراء طلب بيانات من واجهة برمجة التطبيقات، يشير ذلك إلى أنّ المكتبة لم تتمكّن من إعادة الاتصال بعد محاولاتها التلقائية.
في هذا السيناريو، عليك تنفيذ منطق إعادة المحاولة في تطبيقك:
- بالنسبة إلى الإجراءات التي يبدأها المستخدم (أثناء الجلسة): استخدِم عمليات إعادة المحاولة البسيطة لطلب واجهة برمجة التطبيقات. قد تكون المشكلة الأساسية مؤقتة.
- بالنسبة إلى الطلبات في الخلفية: نفِّذ عمليات إعادة المحاولة باستخدام خوارزمية الرقود الأسي الثنائي لتجنُّب إرهاق النظام إذا طال انقطاع الاتصال.
إذا لم تفعِّل ميزة إعادة الاتصال التلقائي بالخدمة
لتجنُّب حدوث هذا الخطأ قدر الإمكان، تحقَّق دائمًا من الاتصال بخدمات Google
Play قبل إجراء طلبات باستخدام Play Billing Library من خلال طلب
BillingClient.isReady().
لمحاولة حلّ المشكلة SERVICE_DISCONNECTED
، يجب أن يحاول تطبيق العميل إعادة إنشاء الاتصال باستخدام
BillingClient.startConnection.
كما هو الحال مع SERVICE_TIMEOUT
، استخدِم عمليات إعادة المحاولة البسيطة أو خوارزمية الرقود الأسي الثنائي، استنادًا إلى الإجراء الذي أدّى إلى حدوث الخطأ.
SERVICE_UNAVAILABLE (رمز الخطأ 2)
ملاحظة مهمة:
اعتبارًا من الإصدار 6.0.0 من Google Play Billing Library، لن يتم عرض SERVICE_UNAVAILABLE بعد الآن للمشاكل في الشبكة. يتم عرضه عندما تكون خدمة الفوترة غير متاحة وسيناريوهات SERVICE_TIMEOUT التي تم إيقافها نهائيًا.
المشكلة
يشير هذا الخطأ المؤقت إلى أنّ خدمة الفوترة في Google Play غير متاحة حاليًا. في معظم الحالات، يعني ذلك وجود مشكلة في الاتصال بالشبكة في أي مكان بين جهاز العميل وخدمات الفوترة في Google Play.
الحلّ المحتمَل
عادةً ما تكون هذه المشكلة مؤقتة. أعِد محاولة الطلب باستخدام استراتيجية إعادة المحاولة البسيطة أو خوارزمية الرقود الأسي الثنائي، استنادًا إلى الإجراء الذي عرض الخطأ.
على عكس SERVICE_DISCONNECTED ، لا يتم قطع الاتصال بخدمة الفوترة في Google Play، وما عليك سوى إعادة محاولة أي عملية تتم محاولتها.
BILLING_UNAVAILABLE (رمز الخطأ 3)
المشكلة
يشير هذا الخطأ إلى حدوث خطأ في فوترة المستخدم أثناء عملية الشراء. في ما يلي أمثلة على الحالات التي يمكن أن يحدث فيها ذلك:
- تطبيق "متجر Play" على جهاز المستخدم قديم.
- المستخدم في بلد غير متوافق.
- المستخدم هو مستخدم مؤسسة، وقد أوقف مشرف المؤسسة إمكانية إجراء عمليات الشراء للمستخدمين.
- يتعذّر على Google Play تحصيل الرسوم من طريقة دفع المستخدم. على سبيل المثال، قد تكون بطاقة ائتمان المستخدم منتهية الصلاحية.
- يحظر النظام تطبيق "متجر Play" (على سبيل المثال، في وضع الأطفال المخصّص من قِبل الشركة المصنّعة للجهاز). في هذه الحالة، يتضمّن
BillingResultرسالة تصحيح الأخطاء تم حظر "متجر Play".
الحلّ المحتمَل
من غير المرجّح أن تساعد عمليات إعادة المحاولة التلقائية في هذه الحالة. ومع ذلك، يمكن أن تساعد إعادة المحاولة اليدوية إذا عالج المستخدم الحالة التي أدّت إلى حدوث المشكلة. على سبيل المثال، إذا عدّل المستخدم إصدار "متجر Play" إلى إصدار متوافق، قد تنجح إعادة المحاولة اليدوية للعملية الأولية.
إذا حدث هذا الخطأ عندما لا يكون المستخدم في جلسة، قد لا يكون من المنطقي إعادة المحاولة. عندما تتلقّى الخطأ
BILLING_UNAVAILABLEنتيجةً لمسار الشراء، من المرجّح جدًا أنّ المستخدم تلقّى ملاحظات من Google Play أثناء عملية الشراء وقد يكون على علم بما حدث. في هذه الحالة، يمكنك عرض رسالة خطأ تحدّد حدوث مشكلة وتقديم زر إعادة المحاولة لمنح المستخدم خيار إعادة المحاولة يدويًا بعد حلّ المشكلة.
خطأ (رمز الخطأ 6)
المشكلة
هذا خطأ جسيم يشير إلى حدوث مشكلة داخلية في Google Play نفسه.
الحلّ المحتمَل
في بعض الأحيان، تكون المشاكل الداخلية في Google Play التي تؤدي إلى ERROR
مؤقتة، ويمكن تنفيذ إعادة المحاولة باستخدام خوارزمية الرقود الأسي الثنائي للتخفيف من المشكلة. عندما يكون المستخدمون في جلسة، من الأفضل إجراء إعادة محاولة بسيطة.
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 للتحقّق مما إذا كان المستخدم قد
حصل على المنتج. إذا لم يحصل عليه، استخدِم منطق إعادة المحاولة البسيطة لإعادة محاولة الشراء.
استجابات 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
المشكلة
هذا خطأ جسيم يشير إلى أنّك تستخدم واجهة برمجة تطبيقات بشكل غير صحيح.
على سبيل المثال، يمكن أن يؤدي تقديم مَعلمات غير صحيحة إلى BillingClient.launchBillingFlow إلى حدوث هذا الخطأ.
الحلّ المحتمَل
تأكَّد من أنّك تستخدم طلبات Play Billing Library المختلفة بشكل صحيح. يمكنك أيضًا الاطّلاع على رسالة تصحيح الأخطاء لمزيد من المعلومات عن الخطأ.