We have a unique problem where some users have tried to purchase a sale bundle in our app, but after they finish a generic "Sale" iAP, we have under rare circumstances lost which bundle they were attempting to purchase.
Our bad logic aside, is there a way to "Decline" a purchase instead of acknowledging and consuming the iAP. I know that after 3 days, the purchase gets refunded and cancelled, but it seems like there should be a way to do this code side to avoid needing to wait in these circumstances.
So instead of something like
mBillingClient.consumeAsync(consumeParams, new ConsumeResponseListener() { ...
it would be
mBillingClient.cancelAsync(consumeParams, new ConsumeResponseListener() {
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'm testing an app which has an in-app subscription feature. I used test subscriptions to test the purchase which seem to work. I then wanted to test that the app responds to cancelled subscriptions so I cancelled the subscription from within Play. However the getPurchase() call still returns the purchase object. I'm using the code from the TrivalDrive sample including the IABHelper.
if (refsub != null && refsub.isAutoRenewing()) {
mRefTechSku = REFTECH_SKU;
mAutoRenewEnabled = true;
} else {
mRefTechSku = "";
mAutoRenewEnabled = false;
}
// The user is subscribed if either subscription exists, even if neither is auto
// renewing
mSubscribedToRefSub = (refsub != null && verifyDeveloperPayload(refsub));
mSubscribedToRefSub returns true while I was expecting it to return false. However mAutoRenewEnabled does return false but is that a valid way to check for active subscriptions since we need to keep the app active for the user until the end of the subscription period.
Thanks for the reply. Turns out that for mSubscribedToRefSub to start returning false it can take up to a day from when the Play store shows that the subscription has been cancelled. So it does work but not right away.
Yes, this is correct.
https://developer.android.com/google/play/billing/billing_subscriptions.html#cancellation
It says that canceling subscription means that user should be able to enjoy the subscription till its expiration date (as there will be no refund), but this subscription will not be renewed after that time.
As the expiration time is still the same, the subscription will be returned in getPurchases() method, but the auto-renewal field will be false.
So, till the time subscription is returned by this method, you must provide its content/feature to the subscriber.
I'm currently testing my InApp billing mechanism (using the InApp Billing version 3 API, therefore taking the TrivialDrive example as reference).
I have one managed item, which is upgrade to premium version.
Now, purchasing the item with my test account works, but when I do a cancellation of the entire order in Google checkout afterwards, my code still tells me that the item is purchased an therefore grants the premium features.
Here is how I check for the purchase in my MainActivity. I do not save the purchase state locally somewhere, as I understood that the with the billing API v3, you can query for purchases ad hoc as needed.
#Override
protected void onStart() {
// TODO Auto-generated method stub
super.onStart();
iabHelper = new IabHelper(this, Helper.getPKey());
iabHelper.enableDebugLogging(true);
iabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
#Override
public void onIabSetupFinished(IabResult result) {
Log.d("IAB", "SETUP FINISHED");
if(!result.isSuccess())
{
Log.d("IAB", "SETUP NOT OK");
return;
}
else
Log.d("IAB", "SETUP OK");
iabHelper.queryInventoryAsync(
new QueryInventoryFinishedListener() {
#Override
public void onQueryInventoryFinished(IabResult result, Inventory inv) {
Log.d("IAB", "Query inventory finished.");
if (result.isFailure()) {
Log.d("IAB","Failed to query inventory: " + result);
return;
}
Log.d("IAB", "Query inventory was successful.");
// Do we have the premium upgrade?
boolean mIsPremium = inv.hasPurchase(Helper.premiumSku);
Purchase p = inv.getPurchase(Helper.premiumSku);
if(p != null)
Log.d("IAB PURCHASE STATE", IabHelper.getResponseDesc(p.getPurchaseState()));
else
Log.d("IAB PURCHASE STATE", "Purchase is null");
Log.d("IAB", "User is " + (mIsPremium ? "PREMIUM" : "NOT PREMIUM"));
}
}
);
}
});
}
I keep getting getPurchaseState = 0, which means is Purchased, even one hour after I cancelled the order. Why?
After having waited for about 12 hours and having tried everything suggested here, I was still facing the same issue. What did the trick for me was the following adb command:
adb shell pm clear com.android.vending
I know this is a year old, but none of the answers/tips presented helped me so I thought I would add my solution.
First, I was experiencing the same issue. Namely, made a test purchase, cancelled it, still received a purchase state indicating valid purchase.
What I forgot was that I recently switched the 'License Test Response' field on the settings pane of the Google Play Developer Console from 'RESPOND_NORMALLY' to 'LICENSED'
After switching it back to 'RESPOND_NORMALLY', the purchase state of the cancelled purchase was correctly returned as such.
So, you might want to check that before you try waiting for days
Step 1. Wait approximately 10 minutes; Until you see the "cancelled order" was delivered. in your google wallet.
Sep 15 11:28 AM Cancelled The order was delivered.
Sep 15 11:18 AM Cancelled You cancelled this order. Reason: Customer request to cancel.
Step 2. Logout your test google account on the device and then re-login.
At least that solved my problem.
This problem also occures when using the app on another device with the same account. The item is not received as purchased until the device is restarted, even after hours. If trying to purchase again, the google wallet dialog says "item already owned". The return code from the iabHelper still is "user cancelled" cause the real response from the purchase activity is not given back, just written in the debug log.
else if (resultCode == Activity.RESULT_CANCELED) {
logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
}
So its not possible to react to this google billing dialog cause we are always getting the same result IABHELPER_USER_CANCELED, even if the dialog said "item already owned".
Edit:
I fix it with this:
else if (resultCode == Activity.RESULT_CANCELED) {
logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
if(responseCode == 7)
result = new IabResult(BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED, "Item already owned.");
else
result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
}
So now if the response from the billing dialog is 7 as "Item already owned" i report it back to my listener.
What you can use is the autoRenewing field of the purchase data. According to the documentation:
autoRenewing: Indicates whether the subscription renews automatically. If true, the subscription is active, and will automatically renew on the next billing date. If false, indicates that the user has canceled the subscription.
And this field get updated immediately after the cancellation.
if I see correctly the reference code in the trivialdrivesample is wrong, which would be a shame for the official reference project for in app billing.
if purchase == null it just means it has never been purchased. To get the real information you have to call
purchase.getPurchaseState()
according to here
purchaseState The purchase state of the order. Possible values are 0 (purchased), 1 (canceled), 2 (refunded), or 3 (expired, for subscription purchases only).
It’s already well answered in the Google official docs. Copying the words here.
When the user cancels a subscription, Google Play does not offer a refund for the current billing cycle. Instead, it allows the user to have access to the cancelled subscription until the end of the current billing cycle, at which time it terminates the subscription. For example, if a user purchases a monthly subscription and cancels it on the 15th day of the cycle, Google Play will consider the subscription valid until the end of the 30th day (or other day, depending on the month).
That should explain it all. getPurchase() will still return the purchase data till the end of the current subscription cycle.
I found the following section in the documentation (IAB API v2), but I am not sure if this can be used for IAB API v3. The broadcast might still be sent though.
"... your application can receive an IN_APP_NOTIFY broadcast intent when Google Play receives a refund notification from Google Wallet. In this case, Google Play sends an IN_APP_NOTIFY message to your application. Your application can handle this message the same way it handles responses from an application-initiated REQUEST_PURCHASE message so that ultimately your application receives a PURCHASE_STATE_CHANGED message that includes information about the item that has been refunded. The refund information is included in the JSON string that accompanies the PURCHASE_STATE_CHANGED broadcast intent. Also, the purchaseState field in the JSON string is set to 2."
from: http://developer.android.com/google/play/billing/v2/api.html#billing-action-notify
I noticed the exact same thing:
Making an in-app purchase with a test account -> refunding the purchase with removing access -> getPurchaseState still returns Purchased (even after relogin and restart) and thus the access to the premium features is not removed in my app.
But when I tested the same thing with a real purchase:
Customer made a real purchase -> a couple of weeks later I refunded it -> Customer did not have access to the premium features of my app anymore.
So could it be, that this is only a problem for test purchases?
As of 2022 March 06, Billing Client version 4 API, you might still need to wait a few hours after cancellation until a purchased item is cancelled in Google Play.
Sometimes I saw cleaning the project also helps (In Android Studio: Build menu > Clear project)
However, in case of subscriptions you can also check and adjust the "Grace period" for your product in Google Play Console:
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.