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

عندما يؤدي طلب من 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" إلى إصدار متوافق، قد تنجح إعادة المحاولة اليدوية للعملية الأولية.

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