التعامل مع رموز استجابة الفوترة

عندما يؤدي طلب من Play Billing Library إلى تنفيذ إجراء، تعرض المكتبة ردًا من النوع BillingResult لإبلاغ المطوّرين بالنتيجة. على سبيل المثال، إذا كنت تستخدم queryProductDetailsAsync للحصول على العروض الترويجية المتاحة للمستخدم، سيتضمّن رمز الاستجابة إما رمز OK ويوفّر كائن ProductDetails الصحيح، أو سيتضمّن استجابة مختلفة تشير إلى سبب عدم إمكانية توفير كائن ProductDetails.

ليست كل رموز الاستجابة أخطاء. تقدّم BillingResponseCode صفحة المرجع وصفًا تفصيليًا لكل ردّ من الردود المناقشة في هذا الدليل. في ما يلي بعض الأمثلة على رموز الاستجابة التي لا تشير إلى أخطاء:

  • BillingClient.BillingResponseCode.OK : اكتمل الإجراء الذي تم تنشيطه من خلال المكالمة بنجاح.
  • BillingClient.BillingResponseCode.USER_CANCELED: بالنسبة إلى الإجراءات التي تعرض سلاسل إجراءات واجهة مستخدم "متجر Google Play" للمستخدم، يشير هذا الردّ إلى أنّ المستخدم انتقل بعيدًا عن سلاسل إجراءات واجهة المستخدم هذه بدون إكمال العملية.

عندما يشير رمز الاستجابة إلى حدوث خطأ، يكون السبب أحيانًا مرتبطًا بظروف مؤقتة، وبالتالي يمكن استعادة البيانات. عندما يعرض استدعاء إحدى طرق Play Billing Library القيمة BillingResponseCode التي تشير إلى حالة يمكن استردادها، عليك إعادة محاولة الاستدعاء. في حالات أخرى، لا تُعتبر الشروط مؤقتة، وبالتالي لا يُنصح بإعادة المحاولة.

تتطلّب الأخطاء المؤقتة استراتيجيات مختلفة لإعادة المحاولة استنادًا إلى عوامل مثل ما إذا كان الخطأ يحدث عندما يكون المستخدمون في جلسة، مثلاً عندما يمرّ المستخدم بعملية شراء، أو ما إذا كان الخطأ يحدث في الخلفية، مثلاً عندما تستعلم عن عمليات الشراء الحالية للمستخدم أثناء onResume. يقدّم قسم استراتيجيات إعادة المحاولة أدناه أمثلة على هذه الاستراتيجيات المختلفة، ويقترح قسم الردود BillingResult التي يمكن إعادة محاولة تنفيذها الاستراتيجية الأنسب لكل رمز استجابة.

بالإضافة إلى رمز الاستجابة، تتضمّن بعض ردود الأخطاء رسائل لأغراض تصحيح الأخطاء وتسجيلها.

استراتيجيات إعادة المحاولة

إعادة المحاولة البسيطة

في الحالات التي يكون فيها المستخدم في جلسة، من الأفضل تنفيذ استراتيجية بسيطة لإعادة المحاولة حتى لا يتسبّب الخطأ في تعطيل تجربة المستخدم قدر الإمكان. في هذه الحالة، ننصحك باتّباع استراتيجية بسيطة لإعادة المحاولة مع تحديد الحدّ الأقصى لعدد المحاولات كشرط للخروج.

يوضّح المثال التالي استراتيجية بسيطة لإعادة المحاولة من أجل معالجة خطأ عند إنشاء اتصال BillingClient:

// Initialize the BillingClient.
private val billingClient = BillingClient.newBuilder(context)
    .setListener(this)
    .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
    .build()

private val coroutineScope = kotlinx.coroutines.CoroutineScope(
    kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.Main.immediate
)

private var connectionJob: kotlinx.coroutines.Job? = null

// Establish a connection to Google Play.
fun startBillingConnection() {
    connectionJob?.cancel()
    connectionJob = coroutineScope.launch {
        connectWithRetry()
    }
}

// Suspended helper to perform a single connection attempt
private suspend fun connectBilling(): BillingResult =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (continuation.isActive) {
                    continuation.resume(billingResult)
                }
            }

            override fun onBillingServiceDisconnected() {
                Log.e(TAG, "Google Play Billing Service disconnected")
                if (continuation.isActive) {
                    continuation.resume(
                        BillingResult.newBuilder()
                            .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
                            .setDebugMessage("Service disconnected during connection setup")
                            .build()
                    )
                } else {
                    startBillingConnection()
                }
            }
        })
    }

// Billing connection retry logic. This is a simple max retry pattern
private suspend fun connectWithRetry() {
    val maxTries = 3
    var tries = 1
    var isConnectionEstablished = false
    while (tries <= maxTries && !isConnectionEstablished) {
        val billingResult = connectBilling()
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            isConnectionEstablished = true
            Log.d(TAG, "Billing response OK")
        } else {
            Log.e(TAG, "Billing connection retry failed: ${billingResult.debugMessage}")
            tries++
            if (tries <= maxTries) {
                delay(2000L) // Wait 2 seconds before retrying
            }
        }
    }
}

fun cleanUp() {
    coroutineScope.cancel()
}
// ...

إعادة المحاولة باستخدام خوارزمية الرقود الأسي الثنائي

ننصحك باستخدام التراجع الأسي لعمليات Play Billing Library التي تحدث في الخلفية ولا تؤثّر في تجربة المستخدم أثناء استخدامه التطبيق.

على سبيل المثال، من المناسب تنفيذ ذلك عند إقرار عمليات شراء جديدة لأنّ هذه العملية يمكن أن تحدث في الخلفية، ولا يلزم أن يتم الإقرار في الوقت الفعلي في حال حدوث خطأ.

private suspend fun acknowledge(purchaseToken: String): BillingResult =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        val params = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchaseToken)
            .build()
        billingClient.acknowledgePurchase(params) { billingResult ->
            continuation.resumeWith(Result.success(billingResult))
        }
    }

private suspend fun queryPurchases(productType: String): Pair<BillingResult, List<Purchase>> =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        val params = QueryPurchasesParams.newBuilder()
            .setProductType(productType)
            .build()
        billingClient.queryPurchasesAsync(params) { billingResult, purchaseList ->
            continuation.resumeWith(Result.success(Pair(billingResult, purchaseList)))
        }
    }

suspend fun acknowledgePurchase(purchaseToken: String) {
    val retryDelayMs = 2000L
    val retryFactor = 2
    val maxTries = 3

    var tries = 1
    var currentDelay = retryDelayMs
    var acknowledgePurchaseResult: BillingResult

    do {
        acknowledgePurchaseResult = acknowledge(purchaseToken)
        val playBillingResponseCode = acknowledgePurchaseResult.responseCode

        when (playBillingResponseCode) {
            BillingClient.BillingResponseCode.OK -> {
                Log.i(TAG, "Acknowledgement was successful")
                return
            }

            BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
                Log.d(TAG, "Acknowledgement failed with ITEM_NOT_OWNED")
                val (billingResult, purchaseList) = queryPurchases(BillingClient.ProductType.SUBS)
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    purchaseList.forEach { purchase ->
                        acknowledge(purchase.purchaseToken)
                    }
                }
                return
            }

            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}"
                )
                if (tries < maxTries) {
                    delay(currentDelay)
                    currentDelay *= retryFactor
                    tries++
                } else {
                    break
                }
            }

            else -> {
                Log.e(
                    TAG,
                    "Acknowledgement failed and cannot be retried -- " +
                        "Response Code: ${acknowledgePurchaseResult.responseCode} -- " +
                        "Debug Message: ${acknowledgePurchaseResult.debugMessage}"
                )
                throw Exception("Failed to acknowledge the purchase!")
            }
        }
    } while (tries <= maxTries)

    throw Exception("Failed to acknowledge the purchase after $maxTries attempts!")
}

ردود BillingResult التي يمكن إعادة محاولة تنفيذها

NETWORK_ERROR (رمز الخطأ 12)

المشكلة

يشير هذا الخطأ إلى حدوث مشكلة في الاتصال بالشبكة بين الجهاز وأنظمة Play.

الحل المحتمل

لإجراء عملية الاسترداد، استخدِم عمليات إعادة محاولة بسيطة أو خوارزمية الرقود الأسي الثنائي، وذلك حسب الإجراء الذي أدّى إلى حدوث الخطأ.

SERVICE_TIMEOUT (رمز الخطأ -3)

المشكلة

يشير هذا الخطأ إلى أنّ الطلب قد وصل إلى الحد الأقصى للمهلة المحدّدة قبل أن يتمكّن Google Play من الردّ. قد يكون السبب في ذلك، على سبيل المثال، تأخيرًا في تنفيذ الإجراء المطلوب من خلال طلب "مكتبة الفوترة في Play".

الحل المحتمل

عادةً ما تكون هذه المشكلة مؤقتة. أعِد محاولة الطلب باستخدام استراتيجية رقود بسيطة أو أسية ثنائية، وذلك حسب الإجراء الذي عرض الخطأ.

على عكس SERVICE_DISCONNECTED الموضّحة أدناه، لا يتم قطع الاتصال بخدمة الفوترة في Google Play، ويجب فقط إعادة محاولة تنفيذ أي عملية تم إجراؤها باستخدام Play Billing Library.

SERVICE_DISCONNECTED (رمز الخطأ -1)

المشكلة

يشير هذا الخطأ الفادح إلى أنّ الاتصال بين تطبيق العميل وخدمة &quot;متجر Google Play&quot; عبر BillingClient قد انقطع.

الحل المحتمل

قدّم الإصدار 8.0.0 من Play Billing Library الميزة enableAutoServiceReconnection(). ننصحك بشدة بتفعيل هذه الميزة عند إنشاء BillingClient. يتيح ذلك للمكتبة محاولة إعادة إنشاء الاتصال تلقائيًا عند إجراء طلب بيانات من واجهة برمجة التطبيقات للفوترة أثناء قطع الاتصال بالخدمة، ما يقلّل بشكل كبير من حالات حدوث هذا الخطأ.

Kotlin

val billingClient = BillingClient.newBuilder(context)
    .setListener(listener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()
    )
    .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)

المشكلة

يشير هذا الخطأ إلى حدوث خطأ في فوترة المستخدم أثناء عملية الشراء. في ما يلي أمثلة على الحالات التي يمكن أن يحدث فيها ذلك:

  • تطبيق "متجر Google Play" على جهاز المستخدم قديم.
  • المستخدم في بلد غير معتمد.
  • المستخدم هو مستخدم في مؤسسة، وقد أوقف مشرف المؤسسة إمكانية إجراء عمليات شراء للمستخدمين الذين تقل أعمارهم عن 10 سنوات.
  • تعذّر على Google Play تحصيل الدفعة من طريقة الدفع التي يستخدمها المستخدم. على سبيل المثال، قد تكون انتهت صلاحية بطاقة الائتمان الخاصة بالمستخدم.
  • يحظر النظام تطبيق "متجر Google Play" (على سبيل المثال، في وضع الأطفال المخصّص من المصنّع الأصلي للجهاز). في هذه الحالة، يتضمّن BillingResult رسالة تصحيح الأخطاء تم حظر متجر Google Play.

الحل المحتمل

  • من غير المرجّح أن تساعد عمليات إعادة المحاولة التلقائية في هذه الحالة. ومع ذلك، يمكن أن تساعد إعادة المحاولة يدويًا في حال عالج المستخدم الشرط الذي تسبّب في المشكلة. على سبيل المثال، إذا حدَّث المستخدم إصدار &quot;متجر Google Play&quot; إلى إصدار متوافق، قد ينجح إعادة المحاولة يدويًا لتنفيذ العملية الأولية.

  • إذا حدث هذا الخطأ عندما لا يكون المستخدم في جلسة، قد لا يكون إعادة المحاولة مفيدًا. عندما تتلقّى الخطأ 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

المشكلة

يشير هذا الخطأ غير القابل للاسترداد إلى أنّ ميزة &quot;الفوترة في Google Play&quot; غير متوافقة مع جهاز المستخدم، ومن المحتمل أن يكون ذلك بسبب إصدار قديم من &quot;متجر Google Play&quot;.

على سبيل المثال، قد لا تتوافق بعض أجهزة المستخدمين مع ميزة المراسلة داخل التطبيق.

إجراءات التخفيف المحتملة

استخدِم BillingClient.isFeatureSupported() للتحقّق من توفّر الميزة قبل إجراء طلب إلى مكتبة Play Billing.

when {
    billingClient.isReady -> {
        val billingResult =
            billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            // use Feature
        }
    }
}

USER_CANCELED

المشكلة

نقَر المستخدم خارج واجهة مستخدم مسار الفوترة.

الحل المحتمل

هذه المعلومات مقدَّمة لغرض إعلامك بها فقط، ويمكن أن يتعذّر عرضها بشكل سليم.

ITEM_UNAVAILABLE

المشكلة

لا يمكن للمستخدم شراء اشتراك أو عملية شراء لمرة واحدة من خلال خدمة "الفوترة في Google Play".

إجراءات التخفيف المحتملة

تأكَّد من أنّ تطبيقك يعيد تحميل تفاصيل المنتج من خلال queryProductDetailsAsync على النحو الموصى به. ضَع في اعتبارك عدد المرات التي يتغير فيها كتالوج منتجاتك في إعدادات Play Console لتنفيذ عمليات إعادة تحميل إضافية إذا لزم الأمر. لا تحاول بيع منتجات على خدمة "الفوترة في Google Play" إلا إذا كانت تعرض المعلومات الصحيحة من خلال queryProductDetailsAsync. تحقَّق من إعدادات أهلية المنتج بحثًا عن أي حالات عدم اتساق. على سبيل المثال، قد تبحث عن منتج متاح فقط في منطقة أخرى غير المنطقة التي يحاول المستخدم الشراء منها. لتصبح المنتجات متاحة للشراء، يجب أن تكون نشطة وأن يكون التطبيق الذي تتضمّنه منشورًا ومتاحًا في بلد المستخدم.

في بعض الأحيان، خاصةً أثناء الاختبار، تكون جميع الإعدادات صحيحة في المنتج، ولكن يظل هذا الخطأ يظهر للمستخدمين. قد يرجع السبب إلى تأخّر نشر تفاصيل المنتج على خوادم Google. يُرجى إعادة المحاولة لاحقًا.

DEVELOPER_ERROR

المشكلة

هذا خطأ فادح يشير إلى أنّك تستخدم إحدى واجهات برمجة التطبيقات بشكل غير صحيح. على سبيل المثال، قد يؤدي تقديم مَعلمات غير صحيحة إلى الدالة BillingClient.launchBillingFlow إلى حدوث هذا الخطأ.

الحل المحتمل

تأكَّد من استخدام طلبات البيانات المختلفة في Play Billing Library بشكل صحيح. يمكنك أيضًا الاطّلاع على رسالة تصحيح الأخطاء للحصول على مزيد من المعلومات حول الخطأ.