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

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

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

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

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

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

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

استراتژی‌های امتحان مجدد

امتحان مجدد ساده

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

مثال زیر یک استراتژی ساده برای تلاش مجدد برای مدیریت خطا هنگام ایجاد اتصال BillingClient را نشان می‌دهد:

// Initialize the BillingClient.
private val billingClient = BillingClient.newBuilder(context)
    .setListener(this)
    .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
    .build()

private val coroutineScope = kotlinx.coroutines.CoroutineScope(
    kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.Main.immediate
)

private var connectionJob: kotlinx.coroutines.Job? = null

// Establish a connection to Google Play.
fun startBillingConnection() {
    connectionJob?.cancel()
    connectionJob = coroutineScope.launch {
        connectWithRetry()
    }
}

// Suspended helper to perform a single connection attempt
private suspend fun connectBilling(): BillingResult =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (continuation.isActive) {
                    continuation.resume(billingResult)
                }
            }

            override fun onBillingServiceDisconnected() {
                Log.e(TAG, "Google Play Billing Service disconnected")
                if (continuation.isActive) {
                    continuation.resume(
                        BillingResult.newBuilder()
                            .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED)
                            .setDebugMessage("Service disconnected during connection setup")
                            .build()
                    )
                } else {
                    startBillingConnection()
                }
            }
        })
    }

// Billing connection retry logic. This is a simple max retry pattern
private suspend fun connectWithRetry() {
    val maxTries = 3
    var tries = 1
    var isConnectionEstablished = false
    while (tries <= maxTries && !isConnectionEstablished) {
        val billingResult = connectBilling()
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            isConnectionEstablished = true
            Log.d(TAG, "Billing response OK")
        } else {
            Log.e(TAG, "Billing connection retry failed: ${billingResult.debugMessage}")
            tries++
            if (tries <= maxTries) {
                delay(2000L) // Wait 2 seconds before retrying
            }
        }
    }
}

fun cleanUp() {
    coroutineScope.cancel()
}
// ...

امتحان مجدد با توقف نمایی

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

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

private suspend fun acknowledge(purchaseToken: String): BillingResult =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        val params = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchaseToken)
            .build()
        billingClient.acknowledgePurchase(params) { billingResult ->
            continuation.resumeWith(Result.success(billingResult))
        }
    }

private suspend fun queryPurchases(productType: String): Pair<BillingResult, List<Purchase>> =
    kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
        val params = QueryPurchasesParams.newBuilder()
            .setProductType(productType)
            .build()
        billingClient.queryPurchasesAsync(params) { billingResult, purchaseList ->
            continuation.resumeWith(Result.success(Pair(billingResult, purchaseList)))
        }
    }

suspend fun acknowledgePurchase(purchaseToken: String) {
    val retryDelayMs = 2000L
    val retryFactor = 2
    val maxTries = 3

    var tries = 1
    var currentDelay = retryDelayMs
    var acknowledgePurchaseResult: BillingResult

    do {
        acknowledgePurchaseResult = acknowledge(purchaseToken)
        val playBillingResponseCode = acknowledgePurchaseResult.responseCode

        when (playBillingResponseCode) {
            BillingClient.BillingResponseCode.OK -> {
                Log.i(TAG, "Acknowledgement was successful")
                return
            }

            BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
                Log.d(TAG, "Acknowledgement failed with ITEM_NOT_OWNED")
                val (billingResult, purchaseList) = queryPurchases(BillingClient.ProductType.SUBS)
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    purchaseList.forEach { purchase ->
                        acknowledge(purchase.purchaseToken)
                    }
                }
                return
            }

            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}"
                )
                if (tries < maxTries) {
                    delay(currentDelay)
                    currentDelay *= retryFactor
                    tries++
                } else {
                    break
                }
            }

            else -> {
                Log.e(
                    TAG,
                    "Acknowledgement failed and cannot be retried -- " +
                        "Response Code: ${acknowledgePurchaseResult.responseCode} -- " +
                        "Debug Message: ${acknowledgePurchaseResult.debugMessage}"
                )
                throw Exception("Failed to acknowledge the purchase!")
            }
        }
    } while (tries <= maxTries)

    throw Exception("Failed to acknowledge the purchase after $maxTries attempts!")
}

پاسخ‌های Retriable BillingResult

NETWORK_ERROR (کد خطا ۱۲)

مشکل

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

وضوح احتمالی

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

SERVICE_TIMEOUT (کد خطا -۳)

مشکل

این خطا نشان می‌دهد که درخواست قبل‌از اینکه Google Play بتواند پاسخ دهد به حداکثر زمان انتظار رسیده است. این امر می‌تواند، برای مثال، به‌دلیل تأخیر در اجرای کنش درخواستی توسط تماس «کتابخانه خدمات صورت‌حساب Play» رخ دهد.

وضوح احتمالی

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

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

SERVICE_DISCONNECTED (کد خطا -۱)

مشکل

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

وضوح احتمالی

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

کاتلین

val billingClient = BillingClient.newBuilder(context)
    .setListener(listener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()
    )
    .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 استفاده کنید. مشکل زیربنایی ممکن است موقتی باشد.
  • برای درخواست‌های پس‌زمینه‌ای: اگر قطع ارتباط طولانی شد، برای جلوگیری از بار اضافی بر سیستم، تلاش مجدد با عقب‌گرد نمایی را پیاده‌سازی کنید.
اگر «اتصال مجدد خودکار سرویس» را فعال نکرده باشید

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

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

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

SERVICE_UNAVAILABLE (کد خطا ۲)

نکته مهم:

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

مشکل

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

وضوح احتمالی

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

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

BILLING_UNAVAILABLE (کد خطا ۳)

مشکل

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

  • برنامه «فروشگاه Play» در دستگاه کاربر قدیمی است.
  • کاربر در کشوری پشتیبانی‌نشده است.
  • کاربر، کاربر سازمانی است و سرپرست سازمانی او کاربران را از خرید کردن غیرفعال کرده است.
  • ‫Google Play نمی‌تواند هزینه را از روش پرداخت کاربر کسر کند. برای مثال، ممکن است کارت اعتباری کاربر منقضی شده باشد.
  • برنامه «فروشگاه Play» توسط سیستم مسدود شده باشد (برای مثال، در حالت کودکان سفارشی‌سازی‌شده توسط سازنده اصلی تجهیزات). در این مورد، BillingResult شامل پیام اشکال‌زدایی فروشگاه Play مسدود شده است می‌شود.

وضوح احتمالی

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

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

خطا (کد خطا ۶)

مشکل

این یک خطای مهلک است که نشان‌دهنده مشکل داخلی در خود Google Play است.

وضوح احتمالی

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

ITEM_ALREADY_OWNED

مشکل

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

وضوح احتمالی

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

ITEM_NOT_OWNED

مشکل

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

وضوح احتمالی

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

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

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

FEATURE_NOT_SUPPORTED

مشکل

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

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

کاهش احتمالی

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

when {
    billingClient.isReady -> {
        val billingResult =
            billingClient.isFeatureSupported(BillingClient.FeatureType.IN_APP_MESSAGING)
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            // use Feature
        }
    }
}

USER_CANCELED

مشکل

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

وضوح احتمالی

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

ITEM_UNAVAILABLE

مشکل

اشتراک «خدمات صورت‌حساب Google Play» یا محصول خرید یک‌باره برای خرید این کاربر دردسترس نیست.

کاهش احتمالی

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

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

DEVELOPER_ERROR

مشکل

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

وضوح احتمالی

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