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).
Related
We have an app in production that most of the time works fine when users are purchasing a subscription. But we do get some reports of users not being able to get to the purchase screen.
I have identified that these users always get BillingResponseCode.SERVICE_DISCONNECTED when running this code
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingResponseCode.OK) {
oncomplete()
}
else {
onError()
}
}
override fun onBillingServiceDisconnected() {
retry()
}
}
Rerunning the startConnection method does not help and thus these users are stuck. If they try it later(hours or days) it might work just fine again.
We are running version 4.0 of the billing library.
Does anyone have an idea of why this happens?
In my project I am trying to integrate new version (5.0) of google billing lib, I am following the google example
https://codelabs.developers.google.com/play-billing-codelab#3
as an example there are two functions:
fun queryPurchases() {
if (!billingClient.isReady) {
Log.e(TAG, "queryPurchases: BillingClient is not ready")
}
// Query for existing subscription products that have been purchased.
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS).build()
) { billingResult, purchaseList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
if (!purchaseList.isNullOrEmpty()) {
_purchases.value = purchaseList
} else {
_purchases.value = emptyList()
}
} else {
Log.e(TAG, billingResult.debugMessage)
}
}
}
which should return purchases that the user has previously made and another function is
fun queryProductDetails() {
val params = QueryProductDetailsParams.newBuilder()
val productList = mutableListOf<QueryProductDetailsParams.Product>()
for (product in LIST_OF_PRODUCTS) {
productList.add(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(product)
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
params.setProductList(productList).let { productDetailsParams ->
Log.i(TAG, "queryProductDetailsAsync")
billingClient.queryProductDetailsAsync(productDetailsParams.build(), this)
}
}
}
where as a result I expect to get available products, however, those two functions return empty lists as a result.
I know that these products exist as before the new lib version I used the previous one 4.x.x and it worked.
What am I missing here? Any advice appreciates.
So, eventually in my case it was a configuration issues, I was need to do a few changes (just for debug, this shouldn't be in production)
1)
...
buildTypes {
release {
debuggable true
...
delete this line (if you have)
...
applicationIdSuffix '.feature'
...
I just migrated to V5 and it's working for me.
For queryPurchasesAsync are you sure you have subscription products? You're passing in BillingClient.ProductType.SUBS as product type? Maybe this should be BillingClient.ProductType.INAPP. The code looks ok otherwise.
For queryProductDetailsAsync you're calling it in the loop multiple times instead of calling it once your productList is populated. Also the let is unnecessary, there's no reason to create a new scope. Using map you can simplify the code to:
val products = inAppPurchaseProductIds.map { productId ->
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.INAPP)
.build()
}
val params = QueryProductDetailsParams.newBuilder().setProductList(products).build()
billingClient.queryProductDetailsAsync(params, this)
Note I used BillingClient.ProductType.INAPP. If you have subscriptions, you'd need to change that to BillingClient.ProductType.SUBS.
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
Here is a scenerio , We have multiple teachers on our app . User can purchase 3 different items from teacher which costs $20, $30, $40 . So I created 3 products in google play console . When user purchases
some item how can I know from which teacher he purchased the item from ? I don't see any way to set extra data when purchasing the item . How people generally handles these cases ?
This is the method I use to launch payment screen
fun buyAnItem(activity:Activity,skuDetails: SkuDetails) {
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build()
val responseCode =
billingClient.launchBillingFlow(activity, flowParams)
log(responseCode.toString())
}
I don't see any way to set extra data in SkuDetails or BillingFlowParams.newBuilder()
How ever I saw we can set these 2 parameters we can set for BillingFlowParams.newBuilder() .setObfuscatedAccountId() and .setObfuscatedProfileId() , should I be using these ? It looks like a hack to me .
I want to get back the extra params in Purchase object
override fun onPurchasesUpdated(
billingResult: BillingResult?,
purchases: MutableList<Purchase>?
) {
for (purchase in purchases) {
consumePurchase(purchase)
}
}
}
Seems like using setObfuscatedProfileId and setObfuscatedAccountId is the right way. Set some unique values for different users . maximum of 64 charecters is allowed per each property .
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.setObfuscatedProfileId(profileId) //Some data you want to send
.setObfuscatedAccountId(id) //Some data you want to send
.build()
val responseCode =
billingClient?.launchBillingFlow(activity, flowParams)
Retrieving :- you can retrieve the data by using purchase.accountIdentifiers?.obfuscatedAccountId and purchase.accountIdentifiers?.obfuscatedProfileId
override fun onPurchasesUpdated(
billingResult: BillingResult?,
purchases: MutableList<Purchase>?
) {
if (billingResult?.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
CoroutineScope(Dispatchers.Main).launch {
log(purchase.accountIdentifiers?.obfuscatedAccountId)
log(purchase.accountIdentifiers?.obfuscatedProfileId)
consumePurchase(purchase)
}
}
}
}
Official documentation :- https://developer.android.com/google/play/billing/developer-payload#attribute
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.