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

عندما يؤدي طلب إلى "مكتبة الفوترة في Play" إلى تنفيذ إجراء، تُرسِل المكتبة 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) }
        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_تذكير (رمز الخطأ -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 يملك حاليًا الاشتراك أو المنتج الذي يتم شراؤه لمرة واحدة والذي يحاول شراءه. في معظم الحالات، لا يكون هذا خطأ عابرًا، إلا عندما يكون ناتجًا عن ملف مؤقت قديم في 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. يُرجى إعادة المحاولة لاحقًا.

خطأ مطور البرامج

المشكلة

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

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

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