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

عندما تؤدي طلب البيانات في 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
}

الاستجابات من نتيجة الفوترة القابلة للتحصيل

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

المشكلة

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

الحل المحتمل

لاسترداد العملية، استخدم عمليات إعادة المحاولة البسيطة أو التراجع الأُسيّ، اعتمادًا على الإجراء الذي أدى إلى حدوث الخطأ.

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

المشكلة

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

الحل المحتمل

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

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

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

المشكلة

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

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
    }
  }
}

تم إلغاء طلب المستخدم

المشكلة

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

الحل المحتمل

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

العنصر غير متوفّر

المشكلة

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

التخفيف المحتمل

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

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

AdsBot_ERROR

المشكلة

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

الحل المحتمل

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