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.
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 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.
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 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.
I've very confused on Android's in-appl billing in regard to RESTORE_TRANSACTIONS.
I have this snippet for making a donation within my app:
BillingHelper.requestPurchase(mContext, "donation");
It works great, no issues there. The problem is here, when the purchase is completed I set a boolean value:
if (BillingHelper.latestPurchase.isPurchased()) {
DONATE_VERSION = true;
}
The app works as intended after this, unless the user uninstalls the app. I store the DONATE_VERSION inside shared preferences. Storing the purchase information in a personal database on the internet is not an option.
When the user re-installs the app, the only way they can get the ads removed from donating is by donating again! I don't want this to be the case. I want to be able to query Google for the results of which items (in this case, jut the "donation" item) have been purchased. I call this in onCreate():
BillingHelper.restoreTransactionInformation(BillingSecurity.generateNonce());
But now what? If the user has previously purchased the managed in app purchase of "donation", how can I query google to get the information about which items have been purchased from in-app billing, so that I can set my boolean again? Please be as clear as possible as I've been messing with this, chatting on IRC, and scouring the API's for about 6 hours now and I can't figure this out.
EDIT:
My onReceive() method:
#Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.i(TAG, "Received action: " + action);
if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) {
String signedData = intent.getStringExtra(INAPP_SIGNED_DATA);
String signature = intent.getStringExtra(INAPP_SIGNATURE);
purchaseStateChanged(context, signedData, signature);
} else if (ACTION_NOTIFY.equals(action)) {
String notifyId = intent.getStringExtra(NOTIFICATION_ID);
notify(context, notifyId);
} else if (ACTION_RESPONSE_CODE.equals(action)) {
long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1);
int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, C.ResponseCode.RESULT_ERROR.ordinal());
checkResponseCode(context, requestId, responseCodeIndex);
} else {
Log.e(TAG, "unexpected action: " + action);
}
You will get the transaction info in a PURCHASE_STATE_CHANGED message, just as when you do after a successful purchase. Process as it as usual and set whatever flags/preferences you need to. Also make sure you only call it on first install (when said preferences are missing/null), because calling it often will get your app blocked for a certain period of time.
I just answered a similiar question to this here: https://stackoverflow.com/a/12187921/455886
A typical scenario flow for restore transactions is as follows:
User installs your app.
On first load of your app, you check if you need to restore
purchases.
If you do, send a RESTORE_TRANSACTION synchronous request to Google.
Google will respond with a acknowlegment response to your
RESTORE_TRANSACTION request. (This is only an acknowlegement that
they received your request.)
At this point, you should mark that you had already sent a restore request to Google and no further restores needs to be sent from the app.
Now asynchronously Google will start sending a 'PURCHASE_STATE_CHANGED' event to your app for each in-app purchase the user has previously purchased. This call is the same as what Google would had sent if the user had made that purchase for the first time.
Since it's the same call, your app would pick up the event and handled it normally as if the user has just purchased the in-app product (thereby "restoring" the purchased feature).
In regard to steps 2 and 5, what I've done for my app is to keep a SharedPreference value called 'APP_INITIALISED' that defaults to false. Everytime my app starts up, if 'APP_INITIALISED' is false, I tell Google to RESTORE_TRANSACTION (step 2) then I set APP_INITIALISED to true (step 5).