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

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

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

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

عندما يشير رمز الاستجابة إلى وجود خطأ، يرجع السبب في بعض الأحيان إلى ظروف عابرة، وبالتالي يمكن الاسترداد. عندما يؤدي طلب إلى طريقة مكتبة الفوترة في Play إلى عرض قيمة 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) }
        tries++
      }
    } while (tries <= maxTries && !isConnectionEstablished)
  }
  ...
}

إعادة محاولة الرقود الأسي

ننصحك باستخدام ميزة "الرقود الأسي الثنائي" في عمليات "مكتبة الفوترة في Play" التي تحدث في الخلفية ولا تؤثّر في تجربة المستخدم عندما يكون المستخدم في الجلسة.

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

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_duration (رمز الخطأ -3)

المشكلة

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

الحل المحتمل

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

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

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

المشكلة

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

الحل المحتمل

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

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

استجابات "نتائج الفوترة التي لا يمكن استردادها"

لا يمكنك إصلاح هذه الأخطاء باستخدام منطق إعادة المحاولة.

FEATURE_NOT_SUPPORTED

المشكلة

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

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

إمكانية تخفيف الأثر

استخدِم BillingClient.isFeatureSupported() للتحقّق من دعم الميزات قبل الاتصال بمكتبة الفوترة في Play.

when {
  billingClient.isReady -> {
    if (billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)) {
       // use feature
    }
  }
}

تم الإلغاء من قِبل المستخدم

المشكلة

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

الحل المحتمل

هذه المعلومات مفيدة فقط ويمكن أن تفشل بشكل لطيف.

ITEM_UNAVAILABLE

المشكلة

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

إمكانية تخفيف الأثر

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

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

طوِّر_ERROR

المشكلة

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

الحل المحتمل

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