【问题标题】:purchasesList from BillingClient is null at firstBillingClient 的 purchaseList 最初为空
【发布时间】:2021-01-12 18:42:38
【问题描述】:

我有 BillingHandler 类,您可以在下面看到我使用谷歌的计费库 v.3 处理应用内计费相关逻辑。我正在使用 Koin 在我的应用模块中使用 single { BillingHandler(androidContext()) } 创建一个单例实例。

现在,当我从 SettingsFragment 调用类的 doesUserOwnPremium() 方法时会出现我的问题,该方法使用设置首选项来显示用作购买按钮的首选项。首先,我使用get() 访问billingHandler 实例,然后调用该方法检查用户是否拥有高级产品。我在测试时已经购买了它,但是当我第一次导航到片段时,BillingHandler 类中的purchasesList 为空,因此返回 false。单击首选项并尝试启动计费流程后,loadSKUs() 中的处理程序 if(!ownsProduct()) {..} 逻辑被调用并评估为 false,从而通知我我确实拥有它。

loadSKUs()方法和doesUserOwnPremium()方法在不同时间调用ownsProduct(),每次都返回上述结果。这是为什么?和初始化有关系吗?

SettingsFragment.kt:

class SettingsFragment : SharedPreferences.OnSharedPreferenceChangeListener
    , PreferenceFragmentCompat() {

    private val TAG = SettingsFragment::class.java.simpleName

    // Billing library setup
    private lateinit var billingHandler:BillingHandler

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        purchasePremiumPreference = findPreference(resources.getString(R.string.purchase_premium))!!
        purchasePremiumPreference.isEnabled = false // disable until the client is ready
        billingHandler = get()

        val ownsPremium = billingHandler.doesUserOwnPremium()
        Toast.makeText(requireContext(),"owns product = $ownsPremium",Toast.LENGTH_LONG).show()
        if(!ownsPremium) {
            purchasePremiumPreference.isEnabled = true
            purchasePremiumPreference.setOnPreferenceClickListener {
                billingHandler.startConnection()
                true
            }
        }
    }
}

BillingHandler.kt:

/**
 * Singleton class that acts as an abstraction layer on top of the Billing library V.3 by google.
 */
class BillingHandler(private val context: Context) : PurchasesUpdatedListener {

    // Billing library setup
    private var billingClient: BillingClient
    private var skuList:ArrayList<String> = ArrayList()
    private val sku = "remove_ads" // the sku to sell
    private lateinit var skuDetails: SkuDetails // the details of the sku to sell
    private var ownsPremium = false

    fun doesUserOwnPremium() : Boolean = ownsPremium

    // analytics
    private lateinit var firebaseAnalytics: FirebaseAnalytics

    init {
        skuList.add(sku) // add SKUs to the sku list (only one in this case)
        billingClient = BillingClient.newBuilder(context)
            .enablePendingPurchases()
            .setListener(this)
            .build()
        ownsPremium = ownsProduct()
    }

    /**
     * Attempts to establish a connection to the billing client. If successful,
     * it will attempt to load the SKUs for sale and begin a billing flow if needed.
     * If the connection fails, it will prompt the user to either retry the connection
     * or cancel it.
     */
    fun startConnection() {
        // start the connection
        billingClient.startConnection(object:BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    loadSKUs()
                } else {
                    Toast.makeText(context,"Something went wrong, please try again!",
                        Toast.LENGTH_SHORT).show()
                }
            }

            override fun onBillingServiceDisconnected() {
                Toast.makeText(context,"Billing service disconnected!", Toast.LENGTH_SHORT).show()
                TODO("implement retry policy. Maybe using a dialog w/ retry and cancel buttons")
            }

        })
    }

    /**
     * Loads the skus from the skuList and starts the billing flow
     * for the selected sku(s) if needed.
     */
    private fun loadSKUs() {
        if(billingClient.isReady) { // load the products that the user can purchase
            val skuDetailsParams = SkuDetailsParams.newBuilder()
                .setSkusList(skuList)
                .setType(BillingClient.SkuType.INAPP)
                .build()

            billingClient.querySkuDetailsAsync(skuDetailsParams
            ) { billingResult, skuDetailsList ->
                if(billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList != null && skuDetailsList.isNotEmpty()) {
                    // for each sku details object
                    for(skuDetailsObj in skuDetailsList) {
                        // make sure the sku we want to sell is in the list and do something for it
                        if(skuDetailsObj.sku == sku) {
                            if(!ownsProduct()) { // product not owned
                                skuDetails = skuDetailsObj // store the details of that sku
                                startBillingFlow(skuDetailsObj)
                            } else { // give premium benefits
                                Toast.makeText(context,"You already own Premium!",Toast.LENGTH_SHORT).show()
                            }
                        }
                    }
                }
            }
        } else {
            Toast.makeText(context,"Billing client is not ready. Please try again!",Toast.LENGTH_SHORT).show()
        }
    }

    /**
     * Checks whether or not the user owns the desired sku
     * @return True if they own the product, false otherwise
     */
    private fun ownsProduct(): Boolean {
        var ownsProduct = false
        // check if the user already owns this product
        // query the user's purchases (reads them from google play's cache)
        val purchasesResult: Purchase.PurchasesResult =
            billingClient.queryPurchases(BillingClient.SkuType.INAPP)

        val purchasesList = purchasesResult.purchasesList // get the actual list of purchases

        if (purchasesList != null) {
            for (purchase in purchasesList) {
                if (purchase.sku == sku) {
                    ownsProduct = true
                    break
                }
            }
        } else {
            Toast.makeText(context,"Purchases list was null",Toast.LENGTH_SHORT).show()
        }
        return ownsProduct
    }

    /**
     * Starts the billing flow for the purchase of the desired
     * product.
     * @param skuDetailsObj The SkuDetails object of the selected sku
     */
    private fun startBillingFlow(skuDetailsObj:SkuDetails) {
        val billingFlowParams = BillingFlowParams.newBuilder()
            .setSkuDetails(skuDetailsObj)
            .build()

        billingClient.launchBillingFlow(
            context as Activity,
            billingFlowParams
        )
    }

    override fun onPurchasesUpdated(billingResult: BillingResult, purchasesList: MutableList<Purchase>?) {
        if(billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
            purchasesList != null) {
            for(purchase in purchasesList) {
                handlePurchase(purchase)
            }
        }
    }

    /**
     * Handles the given purchase by acknowledging it if needed .
     * @param purchase The purchase to handle
     */
    private fun handlePurchase(purchase: Purchase) {
        // if the user purchased the desired sku
        if(purchase.sku == sku && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
            if(!purchase.isAcknowledged) { // acknowledge the purchase so that it doesn't get refunded
                val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken(purchase.purchaseToken)
                    .build()
                billingClient.acknowledgePurchase(acknowledgePurchaseParams
                ) { billingResult ->
                    if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) { // log the event using firebase
                        // log event to firebase
                        val eventBundle = Bundle()
                        eventBundle.putString(FirebaseAnalytics.Param.ITEM_ID,"purchase_ack")
                        eventBundle.putString(FirebaseAnalytics.Param.ITEM_NAME,"Purchase acknowledged")
                        eventBundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, "IN_APP_PURCHASES")
                        firebaseAnalytics.logEvent(FirebaseAnalytics.Event.PURCHASE,eventBundle)
                    }
                }
            }
            showPurchaseSuccessDialog()
        }
    }

    /**
     * Shows a dialog to inform the user of the successful purchase
     */
    private fun showPurchaseSuccessDialog() {
        MaterialDialog(context).show {
            title(R.string.premium_success_dialog_title)
            message(R.string.premium_success_dialog_msg)
            icon(R.drawable.ic_premium)
        }
    }
}

【问题讨论】:

    标签: kotlin android-fragments in-app-billing koin


    【解决方案1】:

    在再次挖掘和阅读文档之后,我意识到当我第一次调用 ownsProduct() 方法时,尚未调用计费客户端的 startConnection() 方法,因此查询返回 null 的原因,客户还没有准备好。

    我决定绕过它,只需使用以下方法开始一个虚拟连接,以便在我的 Application 类中设置客户端。这样,当用户进入应用程序的任何地方时,客户就准备好了,我可以得到他/她的实际购买清单。

    fun dummyConnection() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(p0: BillingResult) {
            }
    
            override fun onBillingServiceDisconnected() {
            }
    
        })
    }
    

    我猜这可能会产生不好的副作用,所以我很想就这是否是正确的方法得到一些反馈。顺便说一句,我需要客户尽快准备好,因为我希望能够验证他们在整个应用程序中拥有溢价(禁用广告等)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-08-09
      • 2020-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-29
      • 2012-06-21
      相关资源
      最近更新 更多