I'm using the following BillingClient.
implementation 'com.android.billingclient:billing:1.2.2'
Although I have issued refund as bellow
But I'm still getting the following respond from BillingClient.queryPurchases
{
"orderId": "GPA.3352-2555-5719-25534",
"packageName": "com.yocto.wenote",
"productId": "note_list_promo",
"purchaseTime": 1560501011137,
"purchaseState": 0,
"purchaseToken": "djjneabakdaenkjafajbbclo.AO-J1OzbDNn5WkobYbSqLNzoBokm1F552-CqzfLQuNXK69bhxC-TnOTqdPV1RCl9T2okpSWfRD9RrE0eFhSN8glUbsOM5XUBDRnm_yK2Ohq_uyNuU17i1dc3CBhdeEn9uZCIfD3zY4tF"
}
According to documentation of BillingClient.queryPurchases
Get purchases details for all the items bought within your app. This
method uses a cache of Google Play Store app without initiating a
network request.
This might be the reason. I try with another function - queryPurchaseHistoryAsync wouldn't help either. According to documentation
Returns the most recent purchase made by the user for each SKU, even
if that purchase is expired, canceled, or consumed.
For queryPurchaseHistoryAsync, there are no purchaseState to indicate the following purchase is cancelled!
{
"productId": "note_list_promo",
"purchaseToken": "djjneabakdaenkjafajbbclo.AO-J1OzbDNn5WkobYbSqLNzoBokm1F552-CqzfLQuNXK69bhxC-TnOTqdPV1RCl9T2okpSWfRD9RrE0eFhSN8glUbsOM5XUBDRnm_yK2Ohq_uyNuU17i1dc3CBhdeEn9uZCIfD3zY4tF",
"purchaseTime": 1560501011137,
"developerPayload": null
}
Google should really provide a non-cached version of queryPurchases.
Any idea what I have done wrong? I don't wish users can continue using paid features, after I have issued the refund.
We clear the cache in the following way
private static void clearGooglePlayStoreBillingCacheIfPossible(BillingClient billingClient) {
billingClient.queryPurchaseHistoryAsync(SkuType.INAPP, (responseCode, purchasesList) -> {
});
billingClient.queryPurchaseHistoryAsync(SkuType.SUBS, (responseCode, purchasesList) -> {
});
}
After that, we will call BillingClient.queryPurchases as usual.
Take note, such cache clearing doesn't happen immediately. It may take as long as 24 hours, for the cache to be cleared. Strangely, this important requirement isn't documented any way.
Related
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
When testing subscriptions, it turned out that canceled subscriptions remain active even after their expiration date. At the same time in Google Play subscription list is empty. I tried two popular IAB libs (in-app-billing v3 and android-checkout).
What causes the problem? Is the problem relevant only when testing? Is there a way to check if the subscription is truly active without the need of running own backend?
bp = new BillingProcessor(a,
"xxx",
new BillingProcessor.IBillingHandler() {
...
#Override
public void onBillingInitialized() {
bp.loadOwnedPurchasesFromGoogle();
bp.isSubscribed(planId); // true for expired cancelled subscription that is not listed in google play
}
});
UPD
I implemented in-app billing without external libs by official guidelines (https://developer.android.com/google/play/billing/billing_integrate.html) and now it works as intended although i have to wait some time to cancelled expired subscription become inactive (sometimes an hour, sometimes a day).
you may try this one.
purchase.getPurchaseState()
// 0 (purchased), 1 (canceled)- 2 (refunded).
I have an android app with renewable monthly subscriptions. In this app I want to notify user some info when his subcription continues in next month.
As I can see renewals in merchant center(orderId ends with eg. ..0, ..1), but when querying the inventory my purchase orderId is same as befor eq.
{
"orderId": "GPA.XXXX-YYYY-XXXX-ZZZZZ",
"packageName": "my.packageName",
"productId": "my.sku",
"purchaseTime": 1456398623654,
"purchaseState": 0,
"developerPayload": "mypayload",
"purchaseToken": "token",
"autoRenewing": true
}
What bothers me more is that purchaseTime also doesn't change.
So my question is: If there is any way to detect in app that renewal occured?
Edit:
I'm using Google Play Developer API to get subscription info and then calculate number of renewals myself.
Order id for all recurrences are returned in orderId field of the INAPP_PURCHASE_DATA JSON field (in V3) with each recurring transaction appended by an integer.
Subscription order numbers
To help you track transactions relating to a given subscription,
Google payments provides a base Merchant Order Number for all
recurrences of the subscription and denotes each recurring transaction
by appending an integer as follows:
GPA.1234-5678-9012-34567 (base order number)
GPA.1234-5678-9012-34567..0 (first recurrence orderID)
GPA.1234-5678-9012-34567..1 (second recurrence orderID)
GPA.1234-5678-9012-34567..2 (third recurrence orderID) ...
But due to local caching you might not get the latest information. So try clearing cache from application manager to first see if you get correct purchase information.
Since purchase query this way is not reliable, it makes more sense to call Google Play Developer Purchases.subscriptions: get API from a backend to get Purchases.subscriptions resource which will return expiryTimeMillis of current subscription.
{
"kind": "androidpublisher#subscriptionPurchase",
"startTimeMillis": long,
"expiryTimeMillis": long,
"autoRenewing": boolean,
"priceCurrencyCode": string,
"priceAmountMicros": long,
"countryCode": string,
"developerPayload": string,
"paymentState": integer,
"cancelReason": integer
}
The purchase data for subscriptions is returned only when the subscription is active.
If the subscription expires then you won't get this purchase data when you query the inventory.
Excerpt from the link
If a recurring payment fails (for example, because the customer’s
credit card has become invalid), the subscription does not renew. The
getPurchases() method does not return failed or expired subscriptions.
You can serve in your DB the expiration date, and every time, when you are getting the data, you can check the expiration date with your db's value, and if it is later, then the subscription was renewed))
I struggled to find a solution for the exact implementation of #random's suggestion. It seems to indeed be the only way to have a solid implementation for renewal tracking on Android, but I couldn't find a good approach online on how to do it. For those who want to save some time (cost me 6 hours today), please find my answer below:
1. First step is to add the dependencies:
implementation "com.google.apis:google-api-services-androidpublisher:v3-rev142-1.25.0" // Update based on latest release
implementation "com.google.auth:google-auth-library-oauth2-http:1.12.1" // Update based on latest release
2. Follow these steps to link the Google Play Console with Google Play Developer API (choose the "Use a service account", not "Use OAuth clients" and follow until "Additional information").
3. Download the services JSON file from your Google Cloud service account (click on the account that you set up in the previous step). You can find/create this file under the "Manage Keys" action or the "Keys" tab. Add the exported JSON file in your assets folder in Android
4. Then you can call the Google Play Developer API to query subscriptions like this (important to call from a Thread, didn't work from the UI thread, not sure why):
new Thread(() -> {
InputStream inputStream = context.getAssets().open("service_account_google_play.json"); // JSON file from step 3
GoogleCredentials credentials = GoogleCredentials.fromStream(inputStream)
.createScoped(AndroidPublisherScopes.ANDROIDPUBLISHER);
AndroidPublisher androidPublisher = new AndroidPublisher(
new NetHttpTransport(),
JacksonFactory.getDefaultInstance(),
new HttpCredentialsAdapter(credentials)
);
SubscriptionPurchase purchase = androidPublisher.purchases().subscriptions().get(
context.getPackageName(), subscriptionId, purchaseToken
).execute();
// Check the orderId or check the expiryTimeMillis for renewal check, e.g. purchase.getOrderId();
}).start();
At the risk of being overly descriptive, the subscriptionId is the ID of your subscription in the Play Console (e.g. subscription_monthly or whatever you called it), and the purchaseToken is the token you get from the Purchase token after querying the BillingClient (querying subscriptions is explained in detail here).
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.