I'm using Google's Billing library to make subscriptions inside my app.
After ~~ 3 days google play has refund every subscription of my users, why did it was?
Some code with activity, which make subscriptions:
private var billingClient: BillingClient? = null
private val purchasesUpdateListener = PurchasesUpdatedListener { billingResult, purchases ->
val isSuccessResult = billingResult.responseCode == BillingClient.BillingResponseCode.OK
val hasPurchases = !purchases.isNullOrEmpty()
if (isSuccessResult && hasPurchases) {
purchases?.forEach(::confirmPurchase)
viewModel.hasSubscription.value = true
}
}
private fun confirmPurchase(purchase: Purchase) {
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient?.consumeAsync(consumeParams) { billingResult, _ ->
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
//all done
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdateListener)
.enablePendingPurchases()
.build()
connectToBillingService()
}
private fun connectToBillingService() {
billingClient?.startConnection(this)
}
private fun getPurchases(): List<Purchase> {
val purchasesResult = billingClient?.queryPurchases(BillingClient.SkuType.SUBS)
return purchasesResult?.purchasesList.orEmpty()
}
override fun onBillingSetupFinished(result: BillingResult) {
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
updateSkuMap()
}
}
}
Because you're using Play Billing Library 2.0 or above. Starting from Play Billing Library 2.0, all purchases must be acknowledged within three days. Failure to properly acknowledge purchases will result in purchases being refunded.
Checking Play Billing Library v2.0 release notes and Processing Purchases for more details.
Inside your code, you are using consumeAsync() to confirm a purchase,
which is the correct way for one time consumable product , because consumeAsync() will acknowledge for you automatically.
But for subscriptions, you should use client.acknowledgePurchase() method rather than consumeAsync()
Here is the sample for Kotlin
val client: BillingClient = ...
val acknowledgePurchaseResponseListener: AcknowledgePurchaseResponseListener = ...
suspend fun handlePurchase() {
if (purchase.purchaseState === PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
val ackPurchaseResult = withContext(Dispatchers.IO) {
client.acknowledgePurchase(acknowledgePurchaseParams.build())
}
}
}
}
May be I am too late to answer, but may be reference for other members.
Yes, refund happens in 3 days, when purchase is not acknowledged. And in this case it seems to be a consumable in-app purchase, where purchase is acknowledged locally. I can see in code, that purchase is getting consumed(acknowledged), but consume might be getting fail in ConsumeResponseListener. I would suggest to consume purchase only when purchase object is in 'PURCHASED' state. Which means to apply a check before going to consume it.
purchaseItem.getPurchaseState() == com.android.billingclient.api.Purchase.PurchaseState.PURCHASED
Related
I am new to android and trying to implement in-app billing the first time.
I am using the google play in-app library. https://developer.android.com/google/play/billing/billing_library_overview
I want to implement subscription in-app purchase in kotlin.
I'm not able to resolve the issue: Suspend function 'acknowledgePurchase' should be called only from a coroutine or another suspend function. How can I call this function?
Here is my handlePurchase method:
fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState === Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val params =
AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
billingClient.acknowledgePurchase(params.build()) }
}
}
Assuming you are using kotlin coroutines in your app.
BillingClient#acknowledgePurchase(params.build()) is a suspended function that should only be called from another suspend function or coroutine scope.
Let's say you are using this fun handlePurchase(purchase: Purchase) in a fragment, you can use viewLifecycleOwner.lifecycleScope.launch{} to launch the same.
fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.purchaseToken)
viewLifecycleOwner.lifecycleScope.launch {
val result = billingClient.acknowledgePurchase(params.build())
when (result.responseCode) {
BillingClient.BillingResponseCode.OK -> {
//success flow}
}
else -> {
//error flow
}
}
}
}
}
}
I am trying to add a subscription to my app. I have created a subscription called "pro_subscription" in the Google Play Console and made sure it's enabled. I then created a new Alpha build and released it to myself, with an account I have configured for testing.
We added com.android.vending.BILLING to the permissions and already had the internet permission.
No matter what we've tried, when we try to fetch the subscription in order to purchase it, no results are found.
We have tried to fetch using Android APIs directly, in which mutableList?.isEmpty() is true:
fun querySkuDetails() {
val purchasesUpdatedListener =
PurchasesUpdatedListener { billingResult, purchases ->
// To be implemented in a later section.
}
var billingClient = BillingClient.newBuilder(activity as MainActivity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
val skuList = ArrayList<String>()
skuList.add("subscription")
skuList.add("pro_subscription")
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)
billingClient.querySkuDetailsAsync(params.build(), SkuDetailsResponseListener() { billingResult: BillingResult, mutableList: MutableList<SkuDetails>? ->
if (mutableList?.isEmpty()!!) {
Log.d("OFFERS: ", "mutableList is empty")
return#SkuDetailsResponseListener
}
})
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
And we also tried using the third-party revenuecat by configuring an offering and then fetching it, in which offerings.all.isEmpty() is true:
Purchases.sharedInstance.getOfferingsWith(
onError = { error ->
/* Optional error handling */
throw IllegalArgumentException()
},
onSuccess = { offerings ->
if (offerings.current == null) {
Log.d("OFFERS: ", "offerings.current is null")
}
if (offerings.all.isEmpty()) {
Log.d("OFFERS: ", "offerings.all.size is 0")
}
// Display current offering with offerings.current
Purchases.sharedInstance.purchasePackageWith(
activity as MainActivity,
offerings.current?.getPackage("pro")!!,
onError = { error, userCancelled -> /* No purchase */
if (userCancelled) {
val requestCancelledToast = Toast.makeText(
context,
R.string.purchase_request_cancelled_toast,
Toast.LENGTH_LONG
)
requestCancelledToast.show()
} else {
throw IllegalArgumentException()
}
},
onSuccess = { product, purchaserInfo ->
if (purchaserInfo.entitlements["pro"]?.isActive == true) {
// Unlock that great "pro" content
try {
val adView =
container!!.findViewById<AdView>(R.id.adView)
(adView.parent as ViewGroup).removeView(
adView
)
} catch (e: Exception) {
val errorRemoveToast = Toast.makeText(
context,
R.string.error_remove_ad,
Toast.LENGTH_LONG
)
errorRemoveToast.show()
}
}
})
})
In both cases, no error is printed, caught, or otherwise identified.
Anyone know why we are not able to fetch the purchases based on the above steps? Thanks!
Did you add Library of billing on the app's gradle?
It is;
dependencies {
val billing_version = "4.0.0"
implementation("com.android.billingclient:billing-ktx:$billing_version")
}
It does not work. As simple as that. I used default code given in google docs.
private fun initBilling() {
val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchase ->
Log.d("Billing", billingResult.debugMessage)
}
val billingClient = BillingClient.newBuilder(this#Home)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
Log.d("Billing", billingResult.debugMessage
?: "billing setup finished with response code ${billingResult.responseCode}")
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
CoroutineScope(Dispatchers.IO).launch {
querySkuDetails(billingClient)
}
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
Log.d("Billing", "disconnected")
}
})
}
None of the Logs that I used were printed and no errors were showing. Start connection just does not trigger any methods from BillingClientStateListener.
There is an unknown issue with android billing library v3.0.0. Just switching to 2.1.0 helped.
I'm trying to test in-app subscriptions for an app I'm working on. The problem is that the SkuDetails list comes empty. I've read in the documentation that the app must first be published to alpha before the subscription stuff can be tested.
As I understand it, after the app is published to alpha I can test a build with the same version on the device and can debug and all that. I don't need to download the release build from alpha.
One of the possible issues is that the app seems to need Google approval before being published to alpha (it has the status of Pending Publication). I don't understand the need for approval as it is just an alpha build.
For now I'm just trying to get the SkuDetails list. In GooglePlay Console I have setup a single subscription and set it to Active. Code wise I have this function:
fun setupBilling(context: Context, callback:
(result: BillingResult,skuDetailsList: List<SkuDetails>) -> Unit) {
billingClient = BillingClient.newBuilder(context)
.enablePendingPurchases()
.setListener(this).build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingResponseCode.OK) {
querySkuDetails(billingClient, callback)
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
}
})
}
The querySkuDetails function looks like:
private fun querySkuDetails(client: BillingClient,
callback: (result: BillingResult,
skuDetailsList: List<SkuDetails>) -> Unit) {
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
client.querySkuDetailsAsync(params.build()) {
billingResult, skuDetailsList ->
callback(billingResult,skuDetailsList)
val msg = billingResult.debugMessage
Log.d(TAG, "List Size: ${skuDetailsList.size}")
Log.d(TAG, msg)
}
}
In the above code, skuList is a list of String with a single element that is my subscription identifier from the Play Console.
There is no debugMessage and the list size is always 0 even though it should be 1.
Any help is appreciated.
Thanks!
Razvan
After 4 days since I uploaded the Alpha version on Google Play, Google approved it and now everything works. So the only thing that I had to do was to wait for Google to approve the app (even if it was just an alpha version).
After updating the app billing lib to
implementation 'com.android.billingclient:billing:2.0.1'
I started to see refunds even after 3 days. How is that possible? Google only mentions here that purchse is refunded if user uninstall the app in short period after purchase. I guess 3 days is not a short period.
Users must acknowledge the purchase within 3 days otherwise the subscription will be refunded:
https://developer.android.com/google/play/billing/billing_library_overview#acknowledge
Do the following in PurchasesUpdatedListener
private val purchaseUpdateListener = PurchasesUpdatedListener { billingResult, purchases ->
for (purchase in purchases) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken).build()
billingClient?.acknowledgePurchase(acknowledgePurchaseParams) {
billingResult ->
val billingResponseCode = billingResult.responseCode
val billingDebugMessage = billingResult.debugMessage
Log.v("TAG_INAPP", "response code: $billingResponseCode")
Log.v("TAG_INAPP", "debugMessage : $billingDebugMessage")
}
}
}
This code will send confirmation of the purchase to the google servers. So any purchase made from your app won't be invalid anymore and won't be refunded automatically.