عندما يؤدي طلب من 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".
الحل المحتمل
عادةً ما تكون هذه المشكلة مؤقتة. أعِد محاولة الطلب باستخدام استراتيجية رقود بسيطة أو أسية ثنائية، وذلك حسب الإجراء الذي أدّى إلى ظهور الخطأ.
على عكس SERVICE_DISCONNECTED الموضّحة أدناه، لا يتم قطع الاتصال بخدمة الفوترة في Google Play، ويجب فقط إعادة محاولة أي عملية تم تنفيذها باستخدام Play Billing Library.
SERVICE_DISCONNECTED (رمز الخطأ -1)
المشكلة
يشير هذا الخطأ الفادح إلى أنّ اتصال تطبيق العميل بخدمة "متجر Google Play" عبر 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" إعادة الاتصال تلقائيًا. إذا كنت لا تزال تتلقّى رمز استجابة SERVICE_DISCONNECTED عند إجراء طلب بيانات من واجهة برمجة التطبيقات، يشير ذلك إلى أنّ المكتبة لم تتمكّن من إعادة الاتصال بعد محاولاتها التلقائية.
في هذا السيناريو، عليك تنفيذ منطق إعادة المحاولة في تطبيقك:
- بالنسبة إلى الإجراءات التي يبدأها المستخدم (أثناء الجلسة): استخدِم عمليات إعادة محاولة بسيطة لطلب البيانات من واجهة برمجة التطبيقات. قد تكون المشكلة الأساسية مؤقتة.
- بالنسبة إلى الطلبات التي يتم تنفيذها في الخلفية: نفِّذ عمليات إعادة المحاولة باستخدام خوارزمية الرقود الأسي الثنائي لتجنُّب إرهاق النظام في حال استمرار انقطاع الاتصال لفترة طويلة.
في حال عدم تفعيل ميزة إعادة الربط التلقائي للخدمة
لتجنُّب حدوث هذا الخطأ قدر الإمكان، تحقَّق دائمًا من الاتصال بخدمات Google Play قبل إجراء مكالمات باستخدام "مكتبة الفوترة في Play" من خلال استدعاء 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" على جهاز المستخدم قديم.
- يوجد المستخدم في بلد غير معتمَد.
- المستخدم هو مستخدم في مؤسسة، وقد أوقف مشرف المؤسسة إمكانية إجراء عمليات شراء للمستخدمين الذين تقل أعمارهم عن 18 عامًا.
- يتعذّر على Google 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.
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 بشكل صحيح. يمكنك أيضًا الاطّلاع على رسالة تصحيح الأخطاء للحصول على مزيد من المعلومات حول الخطأ.