Some of my users tell me that my app forgets the purchased subscriptions every now and then. It works for 3-4 days and then it forgets them. This is a very important issue as users might suspect fraud. I am using billing library 4.0.0 and I have implemented the billing logic as per Google's guidelines.
From what I have gathered it happens when for some reason the billing service connection is interrupted. (Play Store is updating for example)
I have managed to replicate this scenario the following way
- Disable internet connection
- Clearing Play Store app data
- Fresh launch of my app.
- Call billingClient.startConnection()
onBillingSetupFinished called with responseCode BILLING_UNAVAILABLE
user sees -> The app says "no subscription purchased"
- Enable internet connection
- re-initialize BillingClient.
onBillingSetupFinished called with responseCode OK. billingClient.isReady() returns true.
- Call billingClient.queryPurchasesAsync() and billingClient.querySkuDetailsAsync().
onSkuDetailsResponse is called with the skuDetailsList filled with all the proper data. However:
onQueryPurchasesResponse is called with empty purchase list -> Again user sees "no subscriptions purchased"
Important If at this point I open Play Store it shows the purchased subscriptions. But the app still gets an empty purchases list.
If I keep calling billingClient.startConnection() or billingClient.queryPurchasesAsync() at some point after about 10 minutes one attempt will succeed and return a non empty purchases list.
Is it possible to tell Play Store to refresh its subscription data for my app? How can someone handle this scenario gracefully ?
You need to call acknowledgePurchase for every purchase.
See the official docs and the article for more details.
Worked for me!
BillingClient billingClient = ...;
billingClient must be ready -> billingClient.isReady() == true;
So, always call billingClient.queryProductDetailsAsync(...) from
#Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
queryProductDetails();
queryPurchases();
}
}
when initialize the billingClient connection! Keep it up :)
I have a one time purchase in my application. It enables all the features of the application.
When a user purchases the sku on their phone... everything works fine. We see the event returned by the billing client and record the purchase of the premium mode via setting a boolean in a local db. The local Google Play cache is updated properly.
Our problem is when they install on another device. Imagine wanting to use it on a tablet and your phone OR you lost your phone and got another.
We have been using the "query purchases" method of the billing client to validate a purchase. They install the app and I would assume that the purchase history would be added to the local cache at this time.
Generally, it works. But a non-negligible amount of users need to restart their phones several times or wait DAYS from Google to get their ##$% together and actually add the purchase history to their cache.
Their docs say that method only queries the local cache. https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases
The Google Play Developer API lets use query for purchases but we need the purchase token in order to validate anything. If Google Play can't return a purchase token since it has no record of a purchase... what do we do?
How does everyone else deal with this problem?
queryPurchases relies on the cache on device. So if a user's device doesn't have cache updated in time, you will see this behavior. To solve it you should use queryPurchaseHistoryAsync inside queryPurchases as follows
override fun onBillingSetupFinished(billingResult: BillingResult) {
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
...
queryPurchasesAsync()
}
...
}
}
fun queryPurchasesAsync() {
val purchasesResult = HashSet<Purchase>()
var result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.INAPP)
if(nullOrEmpty(result)){
queryPurchaseHistoryAsync(...) // response is returned to onPurchaseHistoryResponse
}else{
... // here you add your existing logic to give users access to products and services
}
}
Aside: since you use the tag google-play-developer-api, does your app have a server? If so you could set the boolean on your server instead of using a local cache and grab the entitlement information from your server. The advantage of this approach is that when your app becomes cross-platform then it doesn't matter if the user made a purchase through Android or the web, they will get access to the premium content by checking with your server.
ref: https://github.com/googlesamples/android-play-billing/tree/master/TrivialDriveKotlin
I'am using subscriptions in my application and it is working perfectly during testing. However, I didn't find a way to get user subscription history for all transactions.
Example:
-User subscribed to product id "sub1" for 3 months. (purchaseToken : "X")
-User canceled subscription for same product id "sub1"
-User resubscribed for same product id (purchaseToken : "Y")
In this scenario when querying queryPurchaseHistoryAsync() function it is returning only latest purchase. Also when using this [API][https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/get] it returns only information of a specific purchase token ("Y" retreived from queryPurchaseHistoryAsync()).
Is there any other way to get user subscriptions history (Detailed transactions)
?
Any help would be greatly appreciated
After a lot of searching it turned out that there is no API that returns detailed transactions of specific subscription.
I'm experiencing an intermittent problem with In App Billing Version 3 using IABHelper as provided by Google.
Before Launching a new purchase I check for unconsumed purchases with queryInventoryAsync. If found I consume any unconsumed purchases and begin a new transaction, if not go directly to start a new transaction. 99% of the time it works fine.
Occasionally when queryInventoryAsync returns no unconsumed purchases, the following launchPurchaseFlow fails with Response : 7: Item already owned.
INFO [IABHelper] Starting async operation: refresh inventory
INFO [IABHelper] Querying owned items, item type: inapp
INFO [IABHelper] Package name: jp.co.mycompany.myapp
INFO [IABHelper] Calling getPurchases with continuation token: null
INFO [IABHelper] Owned items response: 0
INFO [IABHelper] Continuation token: null
INFO [IABHelper] Querying SKU details.
INFO [IABHelper] queryPrices: nothing to do because there are no SKUs.
INFO [IABHelper] Querying owned items, item type: subs
INFO [IABHelper] Package name: jp.co.mycompany.myapp
INFO [IABHelper] Calling getPurchases with continuation token: null
INFO [IABHelper] Owned items response: 0
INFO [IABHelper] Continuation token: null
INFO [IABHelper] Querying SKU details.
INFO [IABHelper] queryPrices: nothing to do because there are no SKUs.
INFO [IABHelper] Ending async operation: refresh inventory
INFO [IABHelper] Constructing buy intent for jp.co.mycompany.myapp.myitem, item type: inapp
INFO [IABHelper] Launching buy intent for jp.co.mycompany.myapp.myitem Request code: 1001
INFO [IABHelper] Ending async operation: launchPurchaseFlow
INFO [IABHelper] Purchase canceled - Response: 7:Item Already Owned
queryInventoryAsync is saying the user does not own the item, but launchPurchaseFlow says the user does.
Strangely after a wait (sometimes a few minutes, sometimes more) the problem resolves itself.
Has anyone experienced anything similar?
For reference I'm using the version of IABHelper last updated on March 15.
Do clear cache of Google Play Service app in mobile. It may work for you.
I'm sure such situation indicates bug in your app, but even though the Google IAP is also one of the factors which causes the problem.
Consider such situation: user purchases item, your are processing it on your server then the app crashes before the item has been consumed. The user comes back to the app, you're trying to process the item once again but IAP returns empty Inventory (no purchases at all). User clicks on the item to buy but receives ITEM_ALREADY_OWNED. That's strange ?!
So there's no purchase but the item is already owned, huh? It's for sure something with local caching on the Google IAP side, but it's your app who has caused this problem (app crash). You can wait hours but in most cases this will not solve itself.
There are two possible solutions. The first one is good for developers, which will clear the Google IAP data.
adb shell pm clear com.android.vending
After a few seconds you will be able to purchase the item once again.
The second solution is bad either for developer or end users. You have to restart the device. After a restart wait 20s to 5 minutes and your item will available to purchase again. If this situation occurs on your customer's device ask him/her to either clear Play Store data manually from settings or to restart the device.
Remember the root cause is crash in your app which mess with the IAP purchase flow before the item is consumed. Fix it and the problem will not occur any more.
I implemented in-app billing into my app and am now testing its handling of refunds.
I bought my app's managed in-app billing item with a test account and refunded it. My app got the refund broadcast as expected and it sees that the item was refunded when restoring transactions, so everything is good up to that point.
My problem is that I can't re-buy the item to test other scenarios.
When I try to purchase the item, the Google Play interface comes up and displays an error message saying "You already own this item." with 2 buttons "OK" and "Details".
If I press details, Google Play crashes and I return to my app.
Did anyone have a similar experience?
Is it forbidden for a user to purchase an in-app item if they previously had it refunded?
I was seeing the same issue. GP crash and everything.
In addition to waiting a few hours, you may want to open up 'Google Play' app info and clear cache and clear data. This solved it for me. It appears GP caches purchase information on the device and only checks Google's servers rarely, if ever, for refund information.
Update:
You may also want to kill the Google Play process since it appears to keep purchase info in memory too.
I asked Google about this issue and they told me that it's not possible to re-buy an in-app billing item on Google Play if it was previously refunded.
But when I tried to buy it again about 24 hours later, the purchase went through ...
So it looks like it's possible to re-buy, but only after some delay.
I know this is an old question, but i have been looking for an answer to this same question and eventually came to my own conclusion. Google doesn't spell it out, but I believe they want you to decide on your own logic as to how to handle cancelled and refunded purchases. Another point to keep in mind is that there there is essentially no difference between a consumable and non consumable managed product. All managed products are consumable.
For me, when a user cancels a purchase, or if I decide to give the user a refund, what I want to happen is that 1) the user receives their money back and 2) the user loses access to the paid feature and 3) the user has the option to purchase the feature again if they choose.
What I did was to check the purchaseState of the purchase on my back end server using the in-app billing API. If the returned purchaseState is a 1 (canceled) or 2 (refunded), I consume the purchase in my app. Google handles item 1, giving the user their money back. The logic in my app handles 2, locking access to the paid features. Consuming the purchase handles 3, giving the user the option to purchase the feature again.
The basic gist of it is, when a purchase is sent to my back end server for verification, I check the purchase state. If the purchase state is a 1 or a 2, I return an appropriate code to my app. When my app receives the code indicating the purchase is cancelled or refunded, my app consumes the purchase.
I use the PHP version of the API, so my simplified code to get the purchase state is :
$purchases = $service->purchases_products->get($packageName, $productId, $purchaseToken);
$purchaseState = $purchases->getPurchaseState();
if($purchaseState === 1){
$serverResponseCode = 3;
}
if($purchaseState === 2){
$serverResponseCode = 4;
}
...and then in my app, I check the server response codes.
if(serverResponseCode == 3 || serverResponseCode ==4 ){
lockFeatures();
ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
billingClient.consumeAsync(params, listener);
}
I hope this helps someone else looking for an answer to this problem.
In case somebody needs android and not kotlin code. All the explanation that smitty wrote:
When starting up the application , You have to check queryPurchases and look for the refunded items.
like that:
if (purchase.getPurchaseState() != Purchase.PurchaseState.UNSPECIFIED_STATE)
{
handleConsumablePurchasesAsync(purchasesList);
return false;
}
Than you CONSUME this item.
smitty1 is a Genius
private void handleConsumablePurchasesAsync(List<Purchase> consumables) {
Log.d(TAG, "handleConsumablePurchasesAsync");
for (Purchase purchase : consumables) {
ConsumeParams params = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(params, (billingResult, purchaseToken) -> {
if (billingResult.getResponseCode() == OK) {
Log.d(TAG, "Consumed the old purchase that hasn't already been acknowledged");
} else {
Log.d(TAG, "Error consume the old purchase that hasn't already been acknowledged -> %s" + String.valueOf(billingResult.getResponseCode()));
}
});
}
}
I noticed that by checking the Remove Entitlements field on the refund page, you will be able to re-buy the product outright without waiting as suggested by the accepted answer.