کدهای پاسخ BillingResult را مدیریت کنید

وقتی فراخوانی کتابخانه‌ی پرداخت Play عملی را آغاز می‌کند، کتابخانه یک پاسخ BillingResult برمی‌گرداند تا توسعه‌دهندگان را از نتیجه مطلع کند. برای مثال، اگر از queryProductDetailsAsync برای دریافت پیشنهادهای موجود برای کاربر استفاده کنید، کد پاسخ یا حاوی یک کد OK است و شیء ProductDetails مناسب را ارائه می‌دهد، یا حاوی پاسخ متفاوتی است که دلیل عدم ارائه شیء ProductDetails را نشان می‌دهد.

همه کدهای پاسخ، خطا نیستند. صفحه مرجع BillingResponseCode شرح مفصلی از هر یک از پاسخ‌های مورد بحث در این راهنما ارائه می‌دهد. برخی از نمونه‌های کدهای پاسخ که خطا را نشان نمی‌دهند عبارتند از:

  • BillingClient.BillingResponseCode.OK : عملی که توسط فراخوانی آغاز شده بود با موفقیت انجام شد.
  • BillingClient.BillingResponseCode.USER_CANCELED : برای اقداماتی که جریان‌های رابط کاربری فروشگاه Play را به کاربر نمایش می‌دهند، این پاسخ نشان می‌دهد که کاربر بدون تکمیل فرآیند، از آن جریان‌های رابط کاربری خارج شده است.

وقتی کد پاسخ، خطایی را نشان می‌دهد، علت آن گاهی اوقات به دلیل شرایط گذرا است و بنابراین بازیابی امکان‌پذیر است. وقتی فراخوانی یک متد Play Billing Library مقداری از BillingResponseCode را برمی‌گرداند که نشان‌دهنده یک شرایط قابل بازیابی است، باید فراخوانی را دوباره امتحان کنید. در موارد دیگر، شرایط گذرا در نظر گرفته نمی‌شوند و بنابراین تلاش مجدد توصیه نمی‌شود.

خطاهای گذرا، بسته به عواملی مانند اینکه آیا خطا زمانی رخ می‌دهد که کاربران در جلسه هستند - مثلاً وقتی کاربر در حال انجام یک فرآیند خرید است - یا اینکه خطا در پس‌زمینه رخ می‌دهد - مثلاً وقتی که در حال پرس‌وجو از خریدهای موجود کاربر در طول onResume هستید - استراتژی‌های تلاش مجدد متفاوتی را می‌طلبند. بخش استراتژی‌های تلاش مجدد در زیر نمونه‌هایی از این استراتژی‌های مختلف را ارائه می‌دهد و بخش پاسخ‌های Retriable BillingResult توصیه می‌کند که کدام استراتژی برای هر کد پاسخ بهتر عمل می‌کند.

علاوه بر کد پاسخ، برخی از پاسخ‌های خطا شامل پیام‌هایی برای اشکال‌زدایی و ثبت وقایع هستند.

استراتژی‌های تلاش مجدد

تلاش مجدد ساده

در شرایطی که کاربر در حال استفاده از session است، بهتر است یک استراتژی تلاش مجدد ساده پیاده‌سازی شود تا خطا تا حد امکان تجربه کاربر را مختل نکند. در این صورت، ما یک استراتژی تلاش مجدد ساده با حداکثر تعداد تلاش را به عنوان شرط خروج توصیه می‌کنیم.

مثال زیر یک استراتژی ساده‌ی تلاش مجدد برای مدیریت خطا هنگام برقراری اتصال 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 که در پس‌زمینه اتفاق می‌افتند و در حین حضور کاربر، بر تجربه‌ی کاربری تأثیری ندارند، از روش بازگشت نمایی (exponential backoff) استفاده کنید.

برای مثال، پیاده‌سازی این مورد هنگام تأیید خریدهای جدید مناسب خواهد بود، زیرا این عملیات می‌تواند در پس‌زمینه اتفاق بیفتد و در صورت بروز خطا، نیازی به تأیید در زمان واقعی نیست.

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

خطای شبکه (کد خطای ۱۲)

مشکل

این خطا نشان می‌دهد که مشکلی در اتصال شبکه بین دستگاه و سیستم‌های Play وجود دارد.

قطعنامه احتمالی

برای بازیابی، بسته به اینکه کدام اقدام باعث خطا شده است، از تلاش‌های مجدد ساده یا عقب‌نشینی نمایی استفاده کنید.

SERVICE_TIMEOUT (کد خطا -3)

مشکل

این خطا نشان می‌دهد که درخواست قبل از اینکه گوگل پلی بتواند پاسخ دهد، به حداکثر زمان انقضا رسیده است. این می‌تواند به عنوان مثال به دلیل تأخیر در اجرای اقدام درخواست شده توسط فراخوانی کتابخانه صورتحساب پلی ایجاد شود.

قطعنامه احتمالی

این معمولاً یک مشکل گذرا است. بسته به اینکه کدام اقدام خطا را برگرداند، درخواست را با استفاده از استراتژی backoff ساده یا نمایی دوباره امتحان کنید.

برخلاف SERVICE_DISCONNECTED در زیر، اتصال به سرویس پرداخت گوگل پلی قطع نمی‌شود و شما فقط باید هر عملیاتی که در کتابخانه پرداخت گوگل پلی انجام شده است را دوباره امتحان کنید.

سرویس قطع شد (کد خطا -1)

مشکل

این خطای مهلک نشان می‌دهد که اتصال برنامه کلاینت به سرویس فروشگاه گوگل پلی از طریق BillingClient قطع شده است.

قطعنامه احتمالی

نسخه ۸.۰.۰ کتابخانه صورتحساب Play ویژگی enableAutoServiceReconnection() را معرفی کرد. اکیداً توصیه می‌شود هنگام ساخت BillingClient خود، این ویژگی را فعال کنید. این به کتابخانه اجازه می‌دهد تا هنگام قطع شدن سرویس، هنگام فراخوانی API صورتحساب، به‌طور خودکار سعی در برقراری مجدد اتصال کند و به‌طور قابل‌توجهی وقوع این خطا را کاهش دهد.

کاتلین

val billingClient = BillingClient.newBuilder(context)
    .setListener(listener)
    .enablePendingPurchases()
    .enableAutoServiceReconnection() // Enable automatic service reconnection
    .build()

جاوا

BillingClient billingClient = BillingClient.newBuilder(context)
    .setListener(listener)
    .enablePendingPurchases()
    .enableAutoServiceReconnection() // Enable automatic service reconnection
    .build();
اگر اتصال مجدد خودکار سرویس را فعال کرده‌اید

کتابخانه پرداخت Play به طور خودکار برای اتصال مجدد تلاش خواهد کرد. اگر هنگام برقراری تماس API، همچنان کد پاسخ SERVICE_DISCONNECTED را دریافت می‌کنید، نشان می‌دهد که کتابخانه پس از تلاش‌های خودکار خود قادر به اتصال مجدد نبوده است. در این سناریو، باید منطق تلاش مجدد را در برنامه خود پیاده‌سازی کنید:

  • برای اقدامات آغاز شده توسط کاربر (در جلسه): از تلاش‌های ساده برای فراخوانی API استفاده کنید. مشکل اساسی ممکن است موقتی باشد.
  • برای درخواست‌های پس‌زمینه: تلاش‌های مجدد را با backoff نمایی پیاده‌سازی کنید تا در صورت طولانی شدن قطع ارتباط، از فشار بیش از حد به سیستم جلوگیری شود.
اگر اتصال مجدد خودکار سرویس را فعال نکرده‌اید

برای جلوگیری از این خطا تا حد امکان، همیشه قبل از برقراری تماس با کتابخانه صورتحساب Play، با فراخوانی BillingClient.isReady() اتصال به سرویس‌های Google Play را بررسی کنید.

برای تلاش برای بازیابی از SERVICE_DISCONNECTED ، برنامه‌ی کلاینت شما باید سعی کند با استفاده از BillingClient.startConnection اتصال را دوباره برقرار کند.

درست مانند SERVICE_TIMEOUT ، بسته به اینکه کدام اقدام باعث خطا شده است، از تلاش‌های مجدد ساده یا عقب‌نشینی نمایی استفاده کنید.

سرویس_غیرقابل_دسترس (کد خطای ۲)

نکته مهم:

از نسخه ۶.۰.۰ کتابخانه صورتحساب گوگل پلی، دیگر SERVICE_UNAVAILABLE برای مشکلات شبکه برگردانده نمی‌شود. این مقدار زمانی برگردانده می‌شود که سرویس صورتحساب در دسترس نباشد و سناریوهای موردی SERVICE_TIMEOUT منسوخ شده.

مشکل

این خطای گذرا نشان می‌دهد که سرویس پرداخت گوگل پلی در حال حاضر در دسترس نیست. در بیشتر موارد، این به این معنی است که مشکل اتصال شبکه بین دستگاه کلاینت و سرویس‌های پرداخت گوگل پلی وجود دارد.

قطعنامه احتمالی

این معمولاً یک مشکل گذرا است. بسته به اینکه کدام اقدام خطا را برگرداند، درخواست را با استفاده از استراتژی backoff ساده یا نمایی دوباره امتحان کنید.

برخلاف SERVICE_DISCONNECTED ، اتصال به سرویس پرداخت گوگل پلی قطع نمی‌شود و شما باید هر عملیاتی را که انجام می‌دهید دوباره امتحان کنید.

صورتحساب_غیرقابل_دسترسی (کد خطای ۳)

مشکل

این خطا نشان می‌دهد که در طول فرآیند خرید، خطایی در صورتحساب کاربر رخ داده است. نمونه‌هایی از مواردی که این اتفاق می‌افتد عبارتند از:

  • برنامه Play Store در دستگاه کاربر قدیمی است.
  • کاربر در کشوری است که پشتیبانی نمی‌شود.
  • کاربر، یک کاربر سازمانی است و مدیر سازمانی او، امکان خرید را برای کاربران غیرفعال کرده است.
  • گوگل پلی نمی‌تواند از روش پرداخت کاربر هزینه دریافت کند. برای مثال، ممکن است اعتبار کارت اعتباری کاربر منقضی شده باشد.

قطعنامه احتمالی

بعید است که تلاش‌های مجدد خودکار در این مورد کمکی کنند. با این حال، اگر کاربر شرایطی را که باعث ایجاد مشکل شده است، برطرف کند، یک تلاش مجدد دستی می‌تواند مفید باشد. به عنوان مثال، اگر کاربر نسخه فروشگاه Play خود را به یک نسخه پشتیبانی شده به‌روزرسانی کند، تلاش مجدد دستی برای عملیات اولیه می‌تواند مؤثر باشد.

اگر این خطا زمانی رخ دهد که کاربر در جلسه نیست، تلاش مجدد ممکن است منطقی نباشد. وقتی در نتیجه جریان خرید، خطای BILLING_UNAVAILABLE دریافت می‌کنید، به احتمال زیاد کاربر در طول فرآیند خرید از Google Play بازخورد دریافت کرده و ممکن است از مشکل پیش آمده آگاه باشد. در این حالت، می‌توانید یک پیام خطا نشان دهید که مشخص می‌کند مشکلی پیش آمده است و یک دکمه «دوباره امتحان کنید» ارائه دهید تا پس از رفع مشکل، به کاربر امکان تلاش مجدد دستی را بدهید.

خطا (کد خطای ۶)

مشکل

این یک خطای مهلک است که نشان دهنده یک مشکل داخلی در خود گوگل پلی است.

قطعنامه احتمالی

گاهی اوقات مشکلات داخلی گوگل پلی که منجر به ERROR می‌شوند، گذرا هستند و می‌توان برای کاهش این مشکل، یک تلاش مجدد با یک backoff نمایی پیاده‌سازی کرد. وقتی کاربران در حال جلسه هستند، یک تلاش مجدد ساده ترجیح داده می‌شود.

آیتم_قبلا_مالک_بوده

مشکل

این پاسخ نشان می‌دهد که کاربر گوگل پلی از قبل مالک محصول اشتراکی یا خرید یک‌باره مورد نظر خود است. در بیشتر موارد، این یک خطای گذرا نیست، مگر در مواردی که به دلیل حافظه پنهان قدیمی گوگل پلی ایجاد شود.

قطعنامه احتمالی

برای جلوگیری از وقوع این خطا زمانی که علت مشکل حافظه پنهان نیست، محصولی را که کاربر از قبل مالک آن است، برای خرید پیشنهاد ندهید. هنگام نمایش محصولات موجود برای خرید، حتماً حقوق کاربر را بررسی کنید و آنچه را که کاربر می‌تواند خریداری کند، بر اساس آن فیلتر کنید. هنگامی که برنامه کلاینت به دلیل مشکل حافظه پنهان این خطا را دریافت می‌کند، این خطا باعث می‌شود حافظه پنهان Google Play با آخرین داده‌های backend Play به‌روزرسانی شود. در این مورد، تلاش مجدد پس از خطا باید این مورد گذرای خاص را برطرف کند. پس از دریافت ITEM_ALREADY_OWNED تابع BillingClient.queryPurchasesAsync() را فراخوانی کنید تا بررسی کنید که آیا کاربر محصول را خریداری کرده است یا خیر، و اگر اینطور نیست، یک منطق تلاش مجدد ساده برای تلاش مجدد خرید پیاده‌سازی کنید.

مورد_غیر_مالکانه

مشکل

این پاسخ خرید نشان می‌دهد که کاربر گوگل پلی مالک محصول اشتراکی یا خرید یک‌باره‌ای که قصد جایگزینی، تأیید یا مصرف آن را دارد، نیست. این خطا در بیشتر موارد گذرا نیست، مگر در مواردی که به دلیل ورود حافظه پنهان گوگل پلی به حالت قدیمی ایجاد شود.

قطعنامه احتمالی

وقتی خطا به دلیل مشکل حافظه پنهان (cache) دریافت می‌شود، خطا باعث می‌شود حافظه پنهان گوگل پلی (Google Play) با آخرین داده‌های بک‌اند پلی (Play) به‌روزرسانی شود. تلاش مجدد با یک استراتژی ساده تلاش مجدد پس از خطا، باید این مورد گذرای خاص را حل کند. پس از دریافت ITEM_NOT_OWNED تابع BillingClient.queryPurchasesAsync() را فراخوانی کنید تا بررسی کنید که آیا کاربر محصول را خریداری کرده است یا خیر. اگر خریداری نکرده است، از منطق ساده تلاش مجدد برای تلاش مجدد خرید استفاده کنید.

پاسخ‌های غیرقابل بازیابی BillingResult

شما نمی‌توانید با استفاده از منطق تلاش مجدد، این خطاها را بازیابی کنید.

ویژگی_پشتیبانی_نشده

مشکل

این خطای غیرقابل برگشت نشان می‌دهد که ویژگی پرداخت گوگل پلی در دستگاه کاربر پشتیبانی نمی‌شود، احتمالاً به دلیل نسخه قدیمی پلی استور.

برای مثال، شاید برخی از دستگاه‌های کاربران شما از پیام‌رسانی درون‌برنامه‌ای پشتیبانی نمی‌کنند.

کاهش احتمالی

قبل از فراخوانی کتابخانه‌ی پرداخت Play، BillingClient.isFeatureSupported() برای بررسی پشتیبانی از ویژگی‌ها استفاده کنید.

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

کاربر_لغو_شد

مشکل

کاربر از رابط کاربری جریان صورتحساب خارج شده است.

قطعنامه احتمالی

این فقط جنبه‌ی اطلاع‌رسانی دارد و می‌تواند به طرز ماهرانه‌ای شکست بخورد.

مورد_غیرقابل_دسترس

مشکل

اشتراک پرداخت گوگل پلی یا محصول خرید یک‌باره برای این کاربر قابل خریداری نیست.

کاهش احتمالی

مطمئن شوید که برنامه شما جزئیات محصول را از طریق queryProductDetailsAsync طبق توصیه به‌روزرسانی می‌کند. در پیکربندی Play Console، تعداد دفعات تغییر کاتالوگ محصولات خود را در نظر بگیرید تا در صورت نیاز به‌روزرسانی‌های اضافی را اعمال کنید. فقط سعی کنید محصولاتی را در Google Play Billing بفروشید که اطلاعات صحیح را از طریق queryProductDetailsAsync برمی‌گردانند. پیکربندی واجد شرایط بودن محصول را برای هرگونه تناقض بررسی کنید. به عنوان مثال، ممکن است در حال جستجوی محصولی باشید که فقط برای منطقه‌ای غیر از منطقه‌ای که کاربر سعی در خرید آن دارد، در دسترس است. برای اینکه یک محصول برای خرید در دسترس باشد، باید فعال باشد، برنامه آن منتشر شده باشد و برنامه آن در کشور کاربر در دسترس باشد.

گاهی اوقات، به ویژه هنگام آزمایش، همه چیز در پیکربندی محصول درست است، اما کاربران هنوز این خطا را مشاهده می‌کنند. این ممکن است به دلیل تأخیر در انتشار جزئیات محصول در سرورهای گوگل باشد. بعداً دوباره امتحان کنید.

خطای توسعه‌دهنده

مشکل

این یک خطای مهلک است که نشان می‌دهد شما به طور نادرست از یک API استفاده می‌کنید. برای مثال، ارائه پارامترهای نادرست به BillingClient.launchBillingFlow می‌تواند باعث این خطا شود.

قطعنامه احتمالی

مطمئن شوید که به درستی از فراخوانی‌های مختلف کتابخانه‌ی پرداخت گوگل پلی استفاده می‌کنید. همچنین، برای اطلاعات بیشتر در مورد خطا، پیام اشکال‌زدایی را بررسی کنید.