I'm working with google play subscription and couldn't figure out how to know when user renewed the plan for one more period. I'm giving 3 credit's to use one feature of my app, and each month the user would receive 3 more credit's if renewed the subscription.
I'm monitoring the user plan using this method below, but it caches the user subscription for a while, so I'm afraid of giving the user 3 more credits when he actually did not renewed the subscription.
fun queryPurchases() {
val purchasesResult = mBillingClient?.queryPurchases(BillingClient.SkuType.SUBS)
if (mBillingClient?.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS)?.responseCode == BillingClient.BillingResponseCode.OK) {
if (purchasesResult?.responseCode == BillingClient.BillingResponseCode.OK) {
purchasesResult.purchasesList?.addAll(
purchasesResult.purchasesList)
}
}
if (purchasesResult != null && purchasesResult.purchasesList != null) {
if (purchasesResult.purchasesList.isEmpty()){
purchasedPlan.postValue(null)
}else {
purchasedPlan.postValue(purchasesResult.purchasesList[0])
}
}
}
How can i handle this situation properly? I thought about giving the user 3 credits anyway on the following month and remove 3 credits if suddenly the plan turns out a free plan, but I think this approach would be very abusable
To get the most recent information about subscriptions you can use queryPurchaseHistoryAsync() instead of queryPurchases(). You can combine it with your local caching algorithm to reduce abuse level. Link to documentation
Warning: queryPurchaseHistoryAsync() makes a network call, which could result in a charge to the app user.
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'm implementing Android in app purchases, with BillingClient api.
I successfully set up billing client, like this:
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases()
.setListener(this).build()
And I implement the code I want to make the in app purchase, which in my case is a subscription.
My issue is as follows, I need to check (periodicly) if the user still has an active subscription, or if they canceled it. From what I read, I can do a call of the method, queryPurchases(PRODUCT_ID). I am doing this as follows:
override fun onBillingSetupFinished(billingResult: BillingResult?) {
if (billingResult != null) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK){
// The BillingClient is ready. You can query purchases here.
Log.d(TAG_PURCHASES_ACTIVITY, "BillingClient ready and connected")
billingClient.queryPurchases(MONTHLY_AGENT_SUBSCRIPTION_PRODUCT_ID)
}
else{
Log.d(TAG_PURCHASES_ACTIVITY, "BillingClient ERROR: ${billingResult.responseCode}")
}
}
}
However, I get this message in the console:
W/BillingClient: getPurchase() failed. Response code: 3
Now after researching, many have said that this is coming from old phones, with older versions of google play services. But I am using new phone (Pixel 3a), which is up to date. This phone also has the google account registered in the google play console for testing.
Why I am getting this message?
Also, is this really the correct way to check if the subscription is still active?
I've tried for ages to find this solution, thanks for any help!
I was making a mistake, passing the product id as a parameter to the query request.
This is what I needed to do:
var list = billingClient.queryPurchases(SkuType.SUBS)
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'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 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.