BlogRight arrowRight ArrowAndroid in-app purchases, part 3: retrieving active purchases and subscription change
BlogRight arrowRight ArrowAndroid in-app purchases, part 3: retrieving active purchases and subscription change

Android in-app purchases, part 3: retrieving active purchases and subscription change

Android in-app purchases, part 3: retrieving active purchases and subscription change
Listen to the episode
Android in-app purchases, part 3: retrieving active purchases and subscription change

This is the third article from our series about implementing purchases on Android. In the series, we cover everything there is about how to add purchases to Google Play apps.

  1. Android in-app purchases, part 1: configuration and adding to the project.
  2. Android in-app purchases, part 2: processing purchases with the Google Play Billing Library.
  3. Android in-app purchases, part 3: retrieving active purchases and subscription change.
  4. Android in-app purchases, part 4: error codes from the Billing Library and testing.
  5. Android in-app purchases, part 5: server-side purchase validation.

Today, we’ll look at two more topics related to implementing in-app purchases with Google Billing Library. We’ll start with retrieving the user’s active purchases, that is, their active subscriptions and previously purchased non-consumable products. The consumable products purchased by them aren’t included here, as one would consume them to be able to purchase them again.

Retrieving the active purchases list would allow us to grant the user access to limited features or disable the purchase button for an already purchased product.

⭐️ Download our guide on in-app techniques which will make in-app purchases in your app perfect

Retrieving the purchase list

In the Billing Library, there’s the queryPurchasesAsync(String skuType, PurchasesResponseListener listener) method for retrieving active purchases. Previously, the queryPurchases(String skuType) synchronous method was used, but it’s been deprecated since Billing Library v. 4.0.0 was released. All methods of this kind require specifying the product type, so you’ll have to combine two requests to receive the full list.

Let’s improve our BillingClientWrapper class in a fashion in line with the previous tutorials:

interface OnQueryActivePurchasesListener {
	fun onSuccess(activePurchases: List<Purchase>)
	fun onFailure(error: Error)
}
 
private fun queryActivePurchasesForType(
   @BillingClient.SkuType type: String,
   listener: PurchasesResponseListener
) {
   onConnected {
       billingClient.queryPurchasesAsync(type, listener)
   }
}
 
fun queryActivePurchases(listener: OnQueryActivePurchasesListener) {
   queryActivePurchasesForType(
       BillingClient.SkuType.SUBS
   ) { billingResult, activeSubsList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           queryActivePurchasesForType(
               BillingClient.SkuType.INAPP
           ) { billingResult, nonConsumableProductsList ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                   listener.onSuccess(
                       activeSubsList.apply { addAll(nonConsumableProductsList) }
                   )
               } else {
                   listener.onFailure(
                       Error(billingResult.responseCode, billingResult.debugMessage)
                   )
               }
           }
       } else {
           listener.onFailure(
               Error(billingResult.responseCode, billingResult.debugMessage)
           )
       }
   }
}

In the callback, we’ll get a list of Purchase class objects. Previously, in order to understand which product the purchase corresponds to, we’d have to call its getSku() method and receive the product’s id. However, in 2021, it was announced on Google I/O that it’s now possible to purchase multiple products in one transaction. So in Billing Library 4.0.0, getSku() was replaced with getSkus() that now returns the list of product ids. It also brought the getQuantity() method that returns the product quantity. 

There’s a small nuance to the queryActivePurchases() method. It retrieves purchases from the Play Services’ local cache, which sometimes renders the results outdated, especially if the purchase was initially made from a different device. This can be rectified by a simple hack. We’ll also add the API for retrieving purchase history into the BillingClientWrapper class. 

interface OnQueryPurchaseHistoryListener {
   fun onSuccess(purchaseHistoryList: List<PurchaseHistoryRecord>)
   fun onFailure(error: Error)
}
 
private fun queryPurchasesHistoryForType(
   @BillingClient.SkuType type: String,
   listener: PurchaseHistoryResponseListener
) {
   onConnected {
       billingClient.queryPurchaseHistoryAsync(type, listener)
   }
}
 
fun queryPurchaseHistory(listener: OnQueryPurchaseHistoryListener) {
   queryPurchasesHistoryForType(
       BillingClient.SkuType.SUBS
   ) { billingResult, subsHistoryList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           queryPurchasesHistoryForType(
               BillingClient.SkuType.INAPP
           ) { billingResult, inappHistoryList ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                   listener.onSuccess(
                       subsHistoryList?.apply { addAll(inappHistoryList.orEmpty()) }.orEmpty()
                   )
               } else {
                   listener.onFailure(
                       Error(billingResult.responseCode, billingResult.debugMessage)
                   )
               }
           }
       } else {
           listener.onFailure(
               Error(billingResult.responseCode, billingResult.debugMessage)
           )
       }
   }
}

Here, the callback returns a list containing the last purchase for each product the user has ever bought. It also synchronizes the local cache of active purchases with its current state. Note that the purchase is an instance of PurchaseHistoryRecord, not Purchase.

We can now declare a method for retrieving the newly synchronized active purchases:

fun querySyncedActivePurchases(listener: OnQueryActivePurchasesListener) {
   queryPurchaseHistory(object : OnQueryPurchaseHistoryListener {
       override fun onSuccess(purchaseHistoryList: List<PurchaseHistoryRecord>) {
           queryActivePurchases(listener)
       }

       override fun onFailure(error: Error) {
           listener.onFailure(error)
       }
   })
}

It’s no coincidence we started with retrieving purchases, as the next topic we’ll cover is closely related to this. Let’s discuss the subscription change.

Start for free

Integrate in-app subscriptions in your Android app in 30 minutes with all side cases

Start for free

Subscription change

In the base case, if the user purchases both subscriptions using the method we described in the previous tutorial, both will be activated. That isn’t always the expected behavior. So in the App Store, for example, this is rectified within App Store Connect with subscription groups, just as in the consumable/non-consumable products case. With Play Market products (again, the same as in the consumable/non-consumable case) this logic has to be implemented on the client side.

In our example from the previous tutorial, the subscriptions are only different in their billing period, so it would make sense for them to be mutually exclusive. Let’s improve our BillingClientWrapper class with a method to replace the old subscription with the new one: 

fun changeSubscription(
   activity: Activity,
   newSub: SkuDetails,
   updateParams: BillingFlowParams.SubscriptionUpdateParams
) {
   onConnected {
       activity.runOnUiThread {
           billingClient.launchBillingFlow(
               activity,
               BillingFlowParams.newBuilder().setSkuDetails(newSub)
                   .setSubscriptionUpdateParams(updateParams).build()
           )
       }
   }
}

The only difference it has compared to a regular purchase is the SubscriptionUpdateParams parameter. In the SubscriptionUpdateParams.Builder class, two methods are of interest for us: 

1. setOldSkuPurchaseToken(String purchaseToken): the method into which we pass the purchaseToken of the active purchase where purchase.skus.first() equals the id of the subscription we want to replace. That’s one of the uses for the active purchases list we previously retrieved. 

2. setReplaceSkusProrationMode(int replaceSkusProrationMode): the method into which you are to pass one of the BillingFlowParams.ProrationMode mode’s constants. Let’s take a closer look at these.

Each of these constants defines when the subscription will get canceled, as well as which transactions will be made and on which day. Let’s look at some examples.

In our case, there are 2 kinds of subscriptions: a monthly one for $9.99 and an annual one for $49.99. For convenience, we’ll round them to $10 and $50, respectively. Let’s say that on September 1 the user purchased the monthly subscription, and then decided to switch to an annual one on September 15. Here are the options we have for implementing this: 

ProrationMode.IMMEDIATE_WITH_TIME_PRORATION:

The replacement takes effect immediately. Since it’s been just half a month since the old subscription period started, there’s $5 left unspent. That amounts to 1/10 of the annual subscription price, which is enough to pay for 36 more days. This way, the user will be charged $50 for the annual subscription on October 22. The next charge will occur on October 22, 2022, and so on.

ProrationMode. IMMEDIATE_AND_CHARGE_PRORATED_PRICE:

This mode isn’t suitable for our pricing scheme, since for this to work, the new subscription’s price per period must exceed the old subscription’s price for the same period. In our case, we end up with $50 per year which equals about $4.17 per month and is less than $10 per month. So for this case, let’s assume the annual subscription costs $144. 

The subscription replacement takes effect immediately. Just as in the previous case, the user keeps $5 and half a month of using the old subscription, as if it never got canceled. However, by the new subscription’s pricing, half a month costs $144 / 12 / 2 = $6. The user will be charged for $1 more, and the resulting $6 go into paying for the new subscription. On October 1, the user will be charged $144. The next charge will occur on October 1, 2022, and so on.

ProrationMode.IMMEDIATE_WITHOUT_PRORATION:

The replacement takes effect immediately, with no extra payment. This way, the user will spend the rest of the month with the new subscription at the old price. On October 1, the user will be charged $50. The next charge will occur on October 1, 2022, and so on.

 ProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE:

The replacement takes effect and the user is charged $50 immediately. Since the remaining $5 are equal to 36 days of the annual subscription, the next charge will occur in 1 year and 36 days. 

ProrationMode.DEFERRED:

In the previous tutorial, we left the description of the onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList <Purchase>?) callback with a cliffhanger saying that you can get a null even if the purchase was successful. That's about this exact case. Not anymore, see the update below.

The monthly subscription will be replaced with the annual one on October 1. The user will be charged $50 the same day. However, the onPurchaseUpdated(billingResult: BillingResult, purchaseList: MutableList <Purchase>?) callback will be called immediately,even though the purchaseList parameter will be null. Google has changed the logic since then, so now the purchaseList parameter will contain the purchase of the subscription plan the user is crossgrading from.

In this case, Google strongly recommends to acknowledge the new subscription on the backend upon receiving the SUBSCRIPTION_RENEWED notification that will arrive on October 1 only. As was already mentioned, the subscription will get canceled in 3 days unless acknowledged. If the user never visits the app in early October, there will be no way to perform the acknowledgement on the client side.

We’ll cover all the backend things in the next article, though. However, I would like to suggest you not to just quit this web-page, but have a look at Adapty SDK for Android, which makes implementing in-app purchases into an app easy and allows developers to save weeks of work.

Further reading

How to design paywall to pass review for the App Store
How to design paywall to pass review for the App Store
December 23, 2020
6 min read
Bootstrapped app to 250k$
Bootstrapped app to 250k$
September 13, 2021
10 min read, 1 hour listen
Understanding Churn: 3 Reasons Why Your Customers Leave
Understanding Churn: 3 Reasons Why Your Customers Leave
December 4, 2020
10 min read