BillingClient 的 purchasesList 一开始是空的
purchasesList from BillingClient is null at first
我有 BillingHandler
class,您可以在下方看到我使用 google 的计费库 v.3 处理应用内计费相关逻辑。我正在使用 Koin 在我的应用程序模块中使用 single { BillingHandler(androidContext()) }
创建单例实例。
现在,当我从我的 SettingsFragment
调用 class' doesUserOwnPremium()
方法时,我的问题出现了,该方法使用设置首选项来显示用作购买按钮的首选项。首先,我使用 get()
访问 billingHandler 实例,然后调用该方法来检查用户是否拥有付费产品。我已经在测试时购买了它,但是当我第一次导航到该片段时,BillingHandler
class 中的 purchasesList
为空,因此此 return 为假。单击首选项并尝试启动计费流程后,loadSKUs()
中的处理程序 if(!ownsProduct()) {..}
逻辑被调用并评估为 false,从而通知我我确实拥有它。
loadSKUs()
方法和doesUserOwnPremium()
方法调用ownsProduct()
的时间不同,每次都return以上结果。这是为什么?跟初始化有关系吗?
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)
}
}
}
再次挖掘和阅读文档后,我意识到当我第一次调用 ownsProduct()
方法时,计费客户端的 startConnection()
方法尚未被调用,因此为什么查询返回 null,客户端尚未准备好。
我决定通过简单地使用以下方法开始虚拟连接来绕过它,以便从我的应用程序 class 中设置客户端。这样,当用户到达应用程序的任何位置时,客户端就准备好了,我可以获得 his/hers 实际购买清单。
fun dummyConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(p0: BillingResult) {
}
override fun onBillingServiceDisconnected() {
}
})
}
我猜这可能会产生不良的副作用,所以我很想就这是否是正确的方法获得一些反馈。顺便说一句,我需要客户尽快准备好,因为我希望能够验证他们在整个应用程序中拥有付费(以禁用广告等)。
我有 BillingHandler
class,您可以在下方看到我使用 google 的计费库 v.3 处理应用内计费相关逻辑。我正在使用 Koin 在我的应用程序模块中使用 single { BillingHandler(androidContext()) }
创建单例实例。
现在,当我从我的 SettingsFragment
调用 class' doesUserOwnPremium()
方法时,我的问题出现了,该方法使用设置首选项来显示用作购买按钮的首选项。首先,我使用 get()
访问 billingHandler 实例,然后调用该方法来检查用户是否拥有付费产品。我已经在测试时购买了它,但是当我第一次导航到该片段时,BillingHandler
class 中的 purchasesList
为空,因此此 return 为假。单击首选项并尝试启动计费流程后,loadSKUs()
中的处理程序 if(!ownsProduct()) {..}
逻辑被调用并评估为 false,从而通知我我确实拥有它。
loadSKUs()
方法和doesUserOwnPremium()
方法调用ownsProduct()
的时间不同,每次都return以上结果。这是为什么?跟初始化有关系吗?
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)
}
}
}
再次挖掘和阅读文档后,我意识到当我第一次调用 ownsProduct()
方法时,计费客户端的 startConnection()
方法尚未被调用,因此为什么查询返回 null,客户端尚未准备好。
我决定通过简单地使用以下方法开始虚拟连接来绕过它,以便从我的应用程序 class 中设置客户端。这样,当用户到达应用程序的任何位置时,客户端就准备好了,我可以获得 his/hers 实际购买清单。
fun dummyConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(p0: BillingResult) {
}
override fun onBillingServiceDisconnected() {
}
})
}
我猜这可能会产生不良的副作用,所以我很想就这是否是正确的方法获得一些反馈。顺便说一句,我需要客户尽快准备好,因为我希望能够验证他们在整个应用程序中拥有付费(以禁用广告等)。