Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

billingClient.queryPurchases returns the list with already cancelled subsription #122

Closed
yaroslav-shlapak opened this issue Feb 22, 2018 · 78 comments
Labels
play billing library issues filed against the Google Play Billing Library or services

Comments

@yaroslav-shlapak
Copy link

Steps to reproduce (test account):

  1. Complete subscription process in the app
  2. Cancel subscription in Play Store app and wait till it ends to completion (for monthly test subscriptions 5 minutes for the date of writing)

Expected: Subscription is no longer in the list which billingClient.queryPurchases(SUB_SKU_TYPE) returns
Actual: Subscription is still in the list which billingClient.queryPurchases(SUB_SKU_TYPE) returns (even after ~1 hour)

As it is stated in the documentation queryPurchases works with cache without doing any network calls, this could be a reason for this issue. queryPurchaseHistoryAsync could be a good alternative but Purchase class doesn't have state of the actual purchase (i.e. SUBSCRIBED, CANCELLED, etc.).

@amed500
Copy link

amed500 commented Mar 7, 2018

i confirm this issue, i have the same problem, and there is another issue related to this method, if we already made a purchase with another device and we want to make another purchase with other device lets say before the other purchase i want to check the purchase list of the same google account, so its empty which is not correct because i have a purchase active in other device .. i don't know if its clear or not but at the end i wanted to ask why we don't have method as before to QueryPurchasesAsync() and not from the cache ?

@phrozenra
Copy link

Has anyone found any workaround for this? Or does anyone know how long it takes for the cache to invalidate itself?

@Bevor
Copy link

Bevor commented Mar 26, 2018

Is this actually just a problem in testing or in production too? I guess the latter one.
I think there is no solution to handle this in IabHelper, because the problem is IInAppBillingService and not IabHelper which is only a wrapper. If developers would be able to handle this, it would mean to somehow trigger Google Play Services to clean the cache (if this is the culprit), but there are no methods for this. Therefore, you can't clean the cache programmatically (Probably this would have impact on other apps, if developers could do this).

In my case, the only problem with the cache is, that IabHelper wants to up- or downgrade a purchase which does not exist anymore. The solution can only be to query the information from another source (e.g. your backend) and if there are no purchases, don't pass the wrongly cached purchase as oldSkus to launchPurchaseFlow(Activity act, String sku, String itemType, List<String> oldSkus, int requestCode, OnIabPurchaseFinishedListener listener, String extraData).
I think this should work, because the billing process itself does not care about the cache. If it would, it would try to upgrade the cached purchase, but this does not happen, because I get an error response of subscription Activity: "We are unable to change your subscription plan", so the billing service itself knows that there are no subscriptions.
I will implement this in this way and will see what happens.

@ashughes
Copy link

Not all apps have a backend. So while this may work, it's not a universal solution.

@phrozenra
Copy link

phrozenra commented Mar 26, 2018

And how would the backend be informed about a purchase being canceled or refunded? While all the payments go through Play Billing (new or old version), there's no trigger / callback / function that we can call that would inform the app or backend that one specific purchase was canceled.

Since the Play Store cache related to purchases gets invalidated correctly every time a purchase is made, it should also be invalidated when a cancellation / refund occurs.

@Bevor
Copy link

Bevor commented Mar 26, 2018

@ashughes That's true. I don't see any other solution for now. If anyone knows a better way, please tell us.

@phrozenra When someone makes a purchase, I always transfer the purchase data to my backend. There you can query all necessary information about the purchase, for example if it's auto renewing, or if it was cancelled:

https://developers.google.com/android-publisher/api-ref/purchases/subscriptions/get.
Response:

{
  "kind": "androidpublisher#subscriptionPurchase",
  "startTimeMillis": long,
  "expiryTimeMillis": long,
  "autoRenewing": boolean,
  "priceCurrencyCode": string,
  "priceAmountMicros": long,
  "countryCode": string,
  "developerPayload": string,
  "paymentState": integer,
  "cancelReason": integer,
  "userCancellationTimeMillis": long,
  "orderId": string,
  "linkedPurchaseToken": string,
  "purchaseType": integer,
  "profileName": string,
  "emailAddress": string,
  "givenName": string,
  "familyName": string,
  "profileId": string
}

(see my solution here https://stackoverflow.com/questions/48176187/server-side-authorization-with-google-play-developer-api how this API can be accessed.)
I run a recurring job in my backend which continuously query this data if a new purchased was saved or in some cases if an existing purchase was changed.

For my solution here: The only thing I hopefully have to do now is to fetch the information, if the user has a 'purchased' purchase record. If yes, then the purchase it allowed to update. If there is no purchase, then I must not pass the oldSkus.

@phrozenra
Copy link

@Bevor thanks for the info, will allow the cache a few days. In my cast, I'm not having subscriptions, but rather one-time purchases, so i'll wait a few days to see if the cache updates by itself. I'm fine with it updating in the 24-72 hours ballpark. Rolling a backend just to check purchase status kinda defeats the purpose of having Google "manage" your products / purchases.

@Bevor
Copy link

Bevor commented Apr 1, 2018

I can confirm, that my workaround I described above works. I can purchase, upgrade, downgrade, cancel, re-purchase consecutively and the currently shown subscription is always the correct one.

(In my case, the purchase concept is very time critical. If a user cancels a subscription but think about re-purchasing again, he will not loose all his data saved as premium customer in my backend if he is doing that within one week. If I would rely on this error-prone Google caching concept, it's not save that a user can re-purchase on time.)

@kennyk
Copy link

kennyk commented Jun 5, 2018

I am facing this issue as well, and it certainly looks like a bug:

  • If I use the Play Store app to change the payment method to "always declines", the subscription will eventually disappear from the list returned by queryPurchases()
  • If I use the Play Store app to cancel the subscription, it is always returned, except autoRenewing is set to false. However, this flag is set to false instantly, so it cannot be used to determine if the user is still within their last-billed subscription period.

I think the solution to integrate with the Developer APIs is a pretty unsatisfactory work around:

  • Suddenly we have to start worrying about usage quotas
  • We are integrating with 2 separate ways of accessing the same system for what is essentially the same data. Which is the source of truth? e.g. can querySkuDetailsAsync() be relied upon, or should we also use the API, GET /inappproducts?

Please fix this, or let us know what is the right way to handle non-active subscriptions in a serverless setup.

@AxesandGrinds
Copy link

In order to fix this, just clear the system cache on your phone. I have the galaxy S8 and follow this process.
After uninstalling it, and then wiping cache, install it again and it should show the updated list correctly.
https://www.youtube.com/watch?v=H65L5XUs_b0

@nicusorflorin
Copy link

any update on this issue?

@phrozenra
Copy link

phrozenra commented Nov 28, 2018

queryPurchases() returns the list of purchases regardless of their status. In order to understand the status of a given purchase, you need to Google Play Developer API. In order to do that, you'll need a client ID, client secret, and a refresh token. You can get the refresh token by calling the Oauth API once using curl on your local device (https://stackoverflow.com/questions/48176187/server-side-authorization-with-google-play-developer-api).

Here's the code I'm using to verify purchases, it's deployed on AWS Lambda using Claudia js:
https://gist.github.com/phrozenra/5f88d880888f8791a0bf9eaf90cc8613

It currently validates properly both one time purchases and subscriptions.

@nicusorflorin
Copy link

@phrozenra in my specific case the subscription returned has the flag autoRenew set to true, but in the manage subscriptions screen it is missing, also our server validation says it's expired, it should either have been autorenewed or the flag should have been false

@phrozenra
Copy link

@nicusorflorin when you say "subscription returned" are you talking about the info returned by queryPurchases() or the info returned by the developer API?

The only issue I've had so far with validating subscriptions was that the subscription was cancelled, payment was refunded, but the access to the subscription was not "revoked" upon refund. So it was still valid for the period of the subscription (until next payment).

Here's a sample of what the developer API returns:
{"kind":"androidpublisher#subscriptionPurchase","startTimeMillis":"1543418766411","expiryTimeMillis":"1574961947567","autoRenewing":true,"priceCurrencyCode":"EUR","priceAmountMicros":"4990000","countryCode":"**","developerPayload":"","paymentState":1,"orderId":"GPA.xxxx-xxxx-xxxx-xxxxx"}

So basically if autoRenewing is true or expiryTimeMillis is in the future, you should grant them access to the product.

@nicusorflorin
Copy link

@phrozenra i mean from queryPurchases(), yes i should in that case, but in my case autoRenewing == true and expiryTimeMillis is in the past

@phrozenra
Copy link

queryPurchases() returns a list from the cache of Google Play Services. It shouldn't be trusted for business decisions (apart from it being a list of purchases). Every purchase on that list needs to be validated individually using the google play developer API.

but in my case autoRenewing == true and expiryTimeMillis is in the past

That's fine, it might be a grace period where Google is waiting for the user to pay. Just trust the autoRenewing = true and trust that Google will flip that switch after the grace period.

@nicusorflorin
Copy link

@phrozenra grace period shouldn't be a thing for test subscriptions, and even so it fails the server validation, and it's missing from the playstore subscription management menu, so autoRenewing should definitely be false. but for some reason it didn't update

@phrozenra
Copy link

I have no clue what’s different for “test“ subscriptions. I’ve simply created a different Google account and did an actual purchase, that I’ve then refunded for testing. Since it’s a test purchase I’m guessing it makes sense not to show up in the real purchases list on PlayStore.

@simcha
Copy link

simcha commented Dec 25, 2018

The workaround for the test only and the test only. Is to go to settings > applications > [whatever you have here] > Google Play Shop. Then Clear Storage. It will not work for your customers but at list you can carry on with your tests.

@JasonWu1111
Copy link

I met the same issue, what I do is clear the google play cache, it works for me :
adb shell pm clear com.android.vending

@perracodex
Copy link

Though the bug was marked as fixed it seems that the issue still exists.
I think would be good if you put any of your findings in the issue tracker regarding this problem:
https://issuetracker.google.com/issues/73982566

@nzackoya
Copy link

nzackoya commented Feb 6, 2019

You can't tell all the users that have this issues to do adb cache clear. What the hell is happening with this? We pay for all the services, do everything following all the docs, why the hell you can't do the only service that helps developers to make better apps just work as expected? Why the hell queryPurchases return only cached purchases and couldn't update 2 days? In that case why the hell onUpdate called with the purchase done immediately????!!!!!? This issue not fixed already for a long time!!! I have made a lot of refunding because of users complains!!!

#127

@SammyO
Copy link

SammyO commented Mar 12, 2019

I can confirm this issue still exists.

@Breefield
Copy link

I can also confirm this issue still exists.

@haykmelqonyan
Copy link

another issue related to caches.
currently Im using this logic.
queryPurchases to receive active subscriptions.
queryPurchaseHistoryAsync to receive subscriptions with status on hold or subscriptions which I cannot receive from queryPurchases by caching issue,
but at this moment I cant receive on hold subscriptions anywhere, queryPurchases is empty
queryPurchaseHistoryAsync returns old subscription without my latest one which is in on hold status, even after end of on hold status and cancelation I cannot receive that package.
the issue is new because previously it works for me, and I dont know when that package will appear in my queryPurchaseHistoryAsync response , which makes testing of on hold state impossible because its only 5 minutes in test purchase.

@vlad-roid
Copy link

another issue related to caches.
currently Im using this logic.
queryPurchases to receive active subscriptions.
queryPurchaseHistoryAsync to receive subscriptions with status on hold or subscriptions which I cannot receive from queryPurchases by caching issue,
but at this moment I cant receive on hold subscriptions anywhere, queryPurchases is empty
queryPurchaseHistoryAsync returns old subscription without my latest one which is in on hold status, even after end of on hold status and cancelation I cannot receive that package.
the issue is new because previously it works for me, and I dont know when that package will appear in my queryPurchaseHistoryAsync response , which makes testing of on hold state impossible because its only 5 minutes in test purchase.

Same issue here, this is a huge problem! I know this was working before in the past, needs to be fixed!

@haykmelqonyan
Copy link

another issue related to caches.
currently Im using this logic.
queryPurchases to receive active subscriptions.
queryPurchaseHistoryAsync to receive subscriptions with status on hold or subscriptions which I cannot receive from queryPurchases by caching issue,
but at this moment I cant receive on hold subscriptions anywhere, queryPurchases is empty
queryPurchaseHistoryAsync returns old subscription without my latest one which is in on hold status, even after end of on hold status and cancelation I cannot receive that package.
the issue is new because previously it works for me, and I dont know when that package will appear in my queryPurchaseHistoryAsync response , which makes testing of on hold state impossible because its only 5 minutes in test purchase.

Same issue here, this is a huge problem! I know this was working before in the past, needs to be fixed!

but google says that it is not a problem and you need to control purchases in your backend , lets move to here to make issue more important,

@wheelergames
Copy link

My issue is with one off purchases, not subscriptions.

If I'm understanding correctly, I can call the cached method of queryPurchases and I will receive the purchase status (purchased, cancelled, refunded etc).
But if I use the uncached call of queryPurchaseHistoryAsync then I get back the purchase but no details on whether it's been refunded or not? Is this correct?

How can I get back the most up to date data on purchases whether they've been refunded or not, and not have to rely on cached data?

@acerasoni
Copy link

For testing i am creating every new app purchase list in the developer console. That is the only way i can test it quickly

Could you please outline how you do this?

@Robokishan
Copy link

created seprate variable for the purchase request. and then use that variable while i am processing in app purchase. on the console side i am creating new in app item to test the in app purchase is having any issue or not because when i first tried it just got refunded but in the app it was saying that it has been purchased so it was very difficult for me to understand how that work. then i saw this issue and realize that it is problem that library caches everything in the playstore. but i could not wait to test for another purchase . so i created another item to purchase and replace the id that is it ! . It seems very unprofessional at first but for testing it is the only work around.

@rayastar
Copy link

Guys don't ask Google fix this bug, it's impossible :)
I found only correct solution - check subscription every time when get it from cache, for example:
String url = String.format("https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s",
"com.alphaenglish.project", mProductId, mPurchaseToken);

answer - simple JSON:
{
"kind": "androidpublisher#subscriptionPurchase",
"startTimeMillis": long,
"expiryTimeMillis": long,
"autoResumeTimeMillis": long,
"autoRenewing": boolean,
"priceCurrencyCode": string,
"priceAmountMicros": long,
"introductoryPriceInfo": {
"introductoryPriceCurrencyCode": string,
"introductoryPriceAmountMicros": long,
"introductoryPricePeriod": string,
"introductoryPriceCycles": integer
}

Where expiryTimeMillis - end subscription time.
If you need additional information - I ready to share it.

@Akif-Efe
Copy link

Akif-Efe commented Aug 28, 2020

Guys don't ask Google fix this bug, it's impossible :)
I found only correct solution - check subscription every time when get it from cache, for example:
String url = String.format("https://www.googleapis.com/androidpublisher/v3/applications/%s/purchases/subscriptions/%s/tokens/%s",
"com.alphaenglish.project", mProductId, mPurchaseToken);

answer - simple JSON:
{
"kind": "androidpublisher#subscriptionPurchase",
"startTimeMillis": long,
"expiryTimeMillis": long,
"autoResumeTimeMillis": long,
"autoRenewing": boolean,
"priceCurrencyCode": string,
"priceAmountMicros": long,
"introductoryPriceInfo": {
"introductoryPriceCurrencyCode": string,
"introductoryPriceAmountMicros": long,
"introductoryPricePeriod": string,
"introductoryPriceCycles": integer
}

Where expiryTimeMillis - end subscription time.
If you need additional information - I ready to share it.

Please can you provide the code sample. How exactly do we do it.

@rayastar
Copy link

rayastar commented Aug 29, 2020

Guys, I suggest create demo app with backend on spring to show how can to control subscription. What do you think about it? Please, give me feedback.

like my post, and if count > 5 I create app + spring server

@Akif-Efe
Copy link

Guys, I suggest create demo app with backend on spring to show how can to control subscription. What do you think about it? Please, give me feedback.

That will be great. I'm waiting for your help.

@rayastar
Copy link

rayastar commented Sep 2, 2020

Guys, I suggest create demo app with backend on spring to show how can to control subscription. What do you think about it? Please, give me feedback.

That will be great. I'm waiting for your help.

Here simple example: https://github.com/xupeng7/GoogleApi
This is a spring boot server

@Shchvova
Copy link

Google at it's best. Before I was using outdated Google Play's billing system with AIDL. That worked amazing, code is messy but it works. No caching issues, just works. Now I am forced to "upgrade" to billing library, and caching issues all over the place. queryPurchases doesn't return a purchase, but when I'm trying to buy the IAP I'm getting ITEM_ALREADY_OWNED error. I don't get how Google hires tens of thousand engineers and still can't make something which was working before.

@fillobotto
Copy link

fillobotto commented Nov 25, 2020

Google at it's best. Before I was using outdated Google Play's billing system with AIDL. That worked amazing, code is messy but it works. No caching issues, just works. Now I am forced to "upgrade" to billing library, and caching issues all over the place. queryPurchases doesn't return a purchase, but when I'm trying to buy the IAP I'm getting ITEM_ALREADY_OWNED error. I don't get how Google hires tens of thousand engineers and still can't make something which was working before.

Still happening to few users every now and then

Yesterday it happened to an user that he could not restore purchases but he was able to buy them again. I am talking about non-consumable products. He gave me his email so I could check his purchases and he had the same purchase twice. This is almost unbelievable.

@ananthakrishnankr
Copy link

Can confirm the issue still exists.

@haykmelqonyan
Copy link

Can confirm issue will exists forever

@ghost
Copy link

ghost commented Jan 19, 2021

Still exists. It's fucking annoying. Mass tech company and absolutely terrible at making something this important work properly. I am on the verge of migrating to iOS.

@ubuntudroid
Copy link

Just migrate to a purchase handling service like RevenueCat and call it a day. 🤷 RevenueCat is free up to a certain amount of money you are making via in-app purchases. No need to build your own server.

@ghost ghost mentioned this issue Jan 20, 2021
@ghost
Copy link

ghost commented Jan 20, 2021

Just migrate to a purchase handling service like RevenueCat and call it a day. 🤷 RevenueCat is free up to a certain amount of money you are making via in-app purchases. No need to build your own server.

Working on it now. Thanks so much for introducing me to this. Life saver <3

@GrosserStuhl
Copy link

Just migrate to a purchase handling service like RevenueCat and call it a day. 🤷 RevenueCat is free up to a certain amount of money you are making via in-app purchases. No need to build your own server.

Does it also cover simple one-time purchases, or only subscriptions? Any other alternatives if I don't have subscriptions?

@vegaro
Copy link

vegaro commented Jan 26, 2021

@GrosserStuhl it works for one time purchases too

@Dimezis
Copy link

Dimezis commented Mar 6, 2021

Folks, I feel your pain, as I experience the same issues, but posting here is meaningless, it's a samples repo, no one accepts library bug reports here.
There's a dedicated issues tracker for these purposes

@Breefield
Copy link

Breefield commented Mar 6, 2021 via email

@pyman2
Copy link

pyman2 commented Mar 6, 2021

The problem seems to be with the samples, not with the library. Subscriptions work fine in my own app (in internal testing at the moment), but I have had to make a lot of changes in the Classy Taxi code after I downloaded it last July. I put the parts of the sample code which I needed into my own app as soon as I got Classy Taxi partially working, and worked on them there, because I didn't want to spend time troubleshooting features which I wasn't going to use. Because of that, I can't copy and paste a solution, but I would say the problem belongs here and not in the issue tracker.

@MathieuRS1
Copy link

Hi guys,

You must check the "revoke" option when refunding on Google Play Console, and the returned list will be correctly updated (queryPurchasesAsync). Without clear cache...

By default the option is not checked, so the user keep his rights (stay in the list returned). It makes sense for consumable products but not for non-consumable with unlocked features (ie with API not free...).

Works properly for me in test and production.

@sirknigget
Copy link

It's September 2021, we are still stuck with this issue.
Using Billing Library 4.0.0.
Tried to query with either queryPurchases (deprecated) or queryPurchasesAsync (new API).
The subscription ended a long time ago, no trace of it anywhere in the console or in Google Play store app, but the Billing Library keeps returning it.

The only thing that sometimes solves this is manually clearing the storage of Google Play store and Google Play Services.
Go tell that to your 100k users...

@jesphinpt
Copy link

jesphinpt commented Oct 8, 2021

billingClient.queryPurchase is returning empty list for me after a successfull payment.

While purchasing, it shows the purchase list values as expected. But after purchase it not showing the purchase list for the app.
How can I check whether the app is purchased or not using the billingClient,

Can anyone kindly let me know.

@aliraza96
Copy link

Still facing this issue.

1 similar comment
@fatasssnake
Copy link

Still facing this issue.

@projectdelta6
Copy link

I'm getting an empty list when I call billingClient.queryPurchasesAsync(...) even thought I can see the active subscription in the Google Play app on the same device

@mil84
Copy link

mil84 commented Apr 8, 2024

Google at it's best. Before I was using outdated Google Play's billing system with AIDL. That worked amazing, code is messy but it works. No caching issues, just works. Now I am forced to "upgrade" to billing library, and caching issues all over the place. queryPurchases doesn't return a purchase, but when I'm trying to buy the IAP I'm getting ITEM_ALREADY_OWNED error. I don't get how Google hires tens of thousand engineers and still can't make something which was working before.

It is 2024, and even after 4 years Google STILL HAS NOT FIXED THIS ˆˆ. This is unbelievable incompetence...I have no words.

I have tried recommended "hack" from here & stackoverflow, calling queryPurchaseHistoryAsync (for both SUBS and INAPPS) before calling queryPurchasesAsync in hope to trigger the cache refresh, but unfortunately it does not work for billing library 6.x :(

Call queryPurchasesAsync is always, no exceptions, returning only cached values => which renders it practically almost useless.

EDIT: For now I came up with this solution:

  1. I stopped using cached queryPurchasesAsync as default check, and instead I'm using network queryPurchaseHistoryAsync as ultimate source of truth, as it's always accurate
  2. Only if user is offline, I call queryPurchasesAsync from cache, otherwise never

So far my testing shows it works fine, just not sure how fool-proof is this solution...

@Bandeapart1964
Copy link

Google at it's best. Before I was using outdated Google Play's billing system with AIDL. That worked amazing, code is messy but it works. No caching issues, just works. Now I am forced to "upgrade" to billing library, and caching issues all over the place. queryPurchases doesn't return a purchase, but when I'm trying to buy the IAP I'm getting ITEM_ALREADY_OWNED error. I don't get how Google hires tens of thousand engineers and still can't make something which was working before.

It is 2024, and even after 4 years Google STILL HAS NOT FIXED THIS ˆˆ. This is unbelievable incompetence...I have no words.

I have tried recommended "hack" from here & stackoverflow, calling queryPurchaseHistoryAsync (for both SUBS and INAPPS) before calling queryPurchasesAsync in hope to trigger the cache refresh, but unfortunately it does not work for billing library 6.x :(

Call queryPurchasesAsync is always, no exceptions, returning only cached values => which renders it practically almost useless.

EDIT: For now I came up with this solution:

  1. I stopped using cached queryPurchasesAsync as default check, and instead I'm using network queryPurchaseHistoryAsync as ultimate source of truth, as it's always accurate
  2. Only if user is offline, I call queryPurchasesAsync from cache, otherwise never

So far my testing shows it works fine, just not sure how fool-proof is this solution...

@mil84 is queryPurchaseHistoryAsync possible know subscriptions are cancelled?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
play billing library issues filed against the Google Play Billing Library or services
Projects
None yet
Development

No branches or pull requests