⟵ Back to blog

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

7 min read

Share

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.

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.

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 subscription change.

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 purchaseList was successful. That’s about this exact case.

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.

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 cancelled 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 things backend 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.

Vlad Guriev
August 30, 2021