android-inapp-billing-v3 icon indicating copy to clipboard operation
android-inapp-billing-v3 copied to clipboard

isSubscribed function is not accurate.

Open castrike opened this issue 7 years ago • 15 comments

When I am checking a set of subscriptions, isSubscribed is returning true for a canceled subscription.

In my application, I have 3 subscriptions models: weekly, monthly and yearly. I have purchased the weekly and monthly subscriptions, but after canceling the weekly one, and waiting a day, the function isSubscribed still returns true.

I think the "issue", please correct me if I am wrong, occurs because the method validates based on the product id being part of the getPurchases function call. getPurchases returns subscriptions that have been canceled too.

If you have purchased one subscription and then canceled it, the signature for the purchase and the subscription are different but the latest one is returned. Verifying subscriptions by the productId does not become adequate since we need to check if the subscription has been canceled and/or the time has expired.

REF: https://developer.android.com/google/play/billing/billing_reference.html#getPurchases

castrike avatar Jun 07 '17 14:06 castrike

If you call billingProcessor.loadOwnedPurchasesFromGoogle(), it will clear the local cache and provide refreshed owned subscriptions and purchases.

autonomousapps avatar Jun 08 '17 19:06 autonomousapps

billingProcessor.loadOwnedPurchasesFromGoogle() uses billingProcessor.getPurchases() in order to set the cache. ( https://github.com/anjlab/android-inapp-billing-v3/blob/master/library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java#L260 )

I have been working directly with the getPurchases function and it still returns a cancelled subscription/ transaction. When verifying the purchaseToken with the Google API, I get a json object like this:

{ "kind": "androidpublisher#subscriptionPurchase", "startTimeMillis": "1496781741401", "expiryTimeMillis": "1496868139044", "autoRenewing": false, "priceCurrencyCode": "USD", "priceAmountMicros": "990000", "countryCode": "US", "developerPayload": "subs:com.myproject.appdevtest.subscription_w", "cancelReason": 0, "userCancellationTimeMillis": "1496844045339" }

The response from getPurchases() is '{"packageName":"com.androidinapptest","productId":"com.myproject.appdevtest.subscription_w","purchaseTime":1496781741401,"purchaseState":0,"developerPayload":"subs:com.myproject.appdevtest.subscription_w","purchaseToken":"somepurchasetoken","autoRenewing":false}'

Could it be because I am testing the app? ( PS: When I initially did the transaction of subscribing, the autoRenewing value was set to true and I had a different purchase token )

castrike avatar Jun 08 '17 19:06 castrike

Any updates ? I face the same problem, where isSubscribed function is not accurate.

jeromeboe avatar Jun 18 '17 09:06 jeromeboe

Have the same problem. Any updates or help?

achatina avatar Jul 22 '17 21:07 achatina

so am i.

verifying the purchaseToken with the Google API.

@castrike how to do that?

didikeeLunaon avatar Aug 01 '17 08:08 didikeeLunaon

I'm calling .listOwnedSubscriptions() but checking every entry with .getSubscriptionTransactionDetails(id).purchaseInfo.purchaseData.autoRenewing, so I know if it has been canceled or not.

moritzgloeckl avatar Aug 01 '17 12:08 moritzgloeckl

having the same problem. isSubscribed() return true after subscription ended.Any help?

edcastrahul avatar Aug 08 '17 11:08 edcastrahul

Same issue here

fedesenmartin avatar Sep 13 '17 19:09 fedesenmartin

No response ?

jewom avatar Jan 25 '18 20:01 jewom

As I said before, call loadOwnedPurchasesFromGoogle(). Yes, it calls getPurchases(), but as you can see, it then calls cacheStorage.clear().

			Bundle bundle = billingService.getPurchases(Constants.GOOGLE_API_VERSION,
														contextPackageName, type, null);
			if (bundle.getInt(Constants.RESPONSE_CODE) == Constants.BILLING_RESPONSE_RESULT_OK)
			{
				cacheStorage.clear();

I can't help more than that. I don't use the isSubscribed() method myself. I have a backend that knows about these things. If it isn't working for you, don't use it. Iterate over the list of owned products and check for validity yourself, based on purchase time, renewal status, etc.

autonomousapps avatar Jan 25 '18 20:01 autonomousapps

I think it's a flawed design of Google Play: it sets purchaseState to 0 (purchased) even for canceled subscriptions (at least for testing subscriptions, I'm not sure about a real subscription). The IAB library simply takes this (probably invalid) data and caches it properly.

The proof: quoting Google Documentation at https://developer.android.com/google/play/billing/billing_reference#getPurchases

  • 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.
  • purchaseState: The purchase state of the order. It always returns 0 (purchased).

So we can't rely on the value of purchaseState since it may be 0 for canceled subscriptions (and it is). We have to rely on autoRenewing.

Therefore I propose to modify the BillingProcessor.isSubscribed() function, so that it not only checks that the cache includes the productId, but the PurchaseData.purchaseState must be PurchaseState.PurchasedSuccessfully AND also (to remedy for this issue) PurchaseState.autoRenewing is true. Exactly as @moritzgloeckl proposes.

mvysny avatar Jul 24 '18 07:07 mvysny

The code in Kotlin would be:

fun BillingProcessor.isSubscribedFix(productId: String) : Boolean {
    // just calling isSubscribed() is not enough because of https://github.com/anjlab/android-inapp-billing-v3/issues/278#issuecomment-407310275
    val transactionDetails: TransactionDetails = getSubscriptionTransactionDetails(productId) ?: return false
    val purchaseData = transactionDetails.purchaseInfo.purchaseData
    return purchaseData.purchaseState == PurchaseState.PurchasedSuccessfully && purchaseData.autoRenewing
}

mvysny avatar Jul 24 '18 07:07 mvysny

I am not sure if this is what you really want, because if a user cancels on the 2nd day of his monthly subscription this code would return false - although he paid for the whole month. The fact that he is not autoRenewing does not mean he isn't subscribed for the rest of the month. The api is (correctly) returning true for isPurchased for the whole month - although the subscription was cancelled.

That said the code above works fine if you want to ask the user why he cancelled the subscription.

aerifiu avatar Aug 29 '20 05:08 aerifiu

Thank you @aerifiu you're absolutely right, I haven't realized that even a canceled subscription is valid until its validity period passes.

mvysny avatar Sep 01 '20 12:09 mvysny

I am not sure if this is what you really want, because if a user cancels on the 2nd day of his monthly subscription this code would return false - although he paid for the whole month. The fact that he is not autoRenewing does not mean he isn't subscribed for the rest of the month. The api is (correctly) returning true for isPurchased for the whole month - although the subscription was cancelled.

That said the code above works fine if you want to ask the user why he cancelled the subscription.

Thank my friend. This comment helped me get exactly the functionality I was looking for.

fifascoutapp avatar Jan 01 '22 02:01 fifascoutapp