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

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

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

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

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

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

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

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

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

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

يوضّح المثال التالي استراتيجية إعادة محاولة بسيطة للتعامل مع خطأ عند إنشاء اتصال 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".

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

المشكلة

يشير هذا الخطأ الفادح إلى انقطاع اتصال تطبيق العميل بخدمة Google Play Store من خلال BillingClient.

الحلّ المحتمل

لتجنّب حدوث هذا الخطأ قدر الإمكان، تحقّق دائمًا من الاتصال بخدمات 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" على جهاز المستخدم قديمًا
  • المستخدم مقيم في بلد غير معتمد.
  • المستخدم هو مستخدم في مؤسسة، وقد أوقف مشرف المؤسسة إمكانية إجراء عمليات الشراء.
  • يتعذّر على Google Play تحصيل الرسوم من طريقة الدفع الخاصة بالمستخدم. على سبيل المثال، قد تكون بطاقة الائتمان الخاصة بالمستخدم منتهية الصلاحية.

الحلّ المحتمل

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

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

خطأ (رمز الخطأ 6)

المشكلة

هذا خطأ فادح يشير إلى مشكلة داخلية في Google Play نفسه.

الحلّ المحتمل

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

ITEM_ALREADY_OWNED

المشكلة

يشير هذا الردّ إلى أنّ مستخدم Google Play يملك الاشتراك أو المنتج الذي يحاول شراؤه لمرّة واحدة. في معظم الحالات، لا يكون هذا خطأ عابرًا، إلا عندما يكون ناتجًا عن ملف cache قديم في 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.

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" بشكل صحيح. يمكنك أيضًا الاطّلاع على رسالة تصحيح الأخطاء للحصول على مزيد من المعلومات حول الخطأ.