Subscriptions 101: learn how to get +23% app revenue Read more

Android in-app purchases, part 1: configuration and adding to the project

Vlad Guriev

January 27, 2021

15 min read

Table of Contents

612ce594ea2d50f58ccda3b0 android tutorial 1 configuration min

In-app purchases, especially subscriptions, are the most popular methods to monetize an app. On the one hand, a subscription allows a developer to invest into developing content and the product, on the other hand, it helps the users to get a more high-quality app in general. In-app purchases are subject to 30% commission, but if a user has been subscribed for more than a year or an app earns less than $1М per year, the commission is 15%.

This is the first article from the series dedicated to in-app purchases in Android apps. In this series, we will cover several topics starting with the creation of in-app purchases and leading up to server validation and analytics:

  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.

In this article, we’ll explain how to:  

  • Create a product in Google Play Console;
  • Configure subscriptions: how to specify duration, price, trials; 
  • Add a list of products in an app. 

Creating subscription

Before we start, make sure you:

  1. Have a developer account for Google Play.
  2. Have signed all agreements and are ready to start working.  

Now, let’s get down to business and create our first product.  

Switch to your developer account and choose the app.

60fef7a69f036b3bfc996258 jsaoezesquwst5gknnib6kc3xqckd8yldedvgpfkaagrqbg9xksbckpwo

Then, in the menu on the left, find the Products section, select Subscriptions and click Create a Subscription

60fef7a7b3b5c7c37e286978 qy go34alibtdmnfdkaii3wg1fcjv5bexstyoefeazq 2oagsnx37gndqykcx6rcwcyu7ahpfzug gusku2bg0sxtq nkmglkw5uuvlabnnlnmp7j2ljfc9 nkzhyhbhegp1nl8r

Then, we’ll see the subscription configurator. Here are some important points.   

60fef7a7d069f50dac0202a5 edgfvm0dhtqew hin9r1uqwvyzuzna1nw4auk hsl8kvgoc2uev8ulj5yhinnvdwv6xau1c1kzy2vd0fmpc8ezyurr3vtl 1qbj19ewbfwqspewed3d echiz1gjqfrfpeiwm0yb
  1. Create the ID that will be used in the app. It’s a good idea to add a subscription period or some other useful information to ID, thus, you can create products in one style, and make analyzing the sales statistics easier.
  2. The name of the subscription that the user will see in the store.  
  3. Subscription description. The user will see it, too.  
60fef7a755430242d074b04c 5mix72nt8rn7nbjrkxd7l7pfit6yuebw9f8x0bke47mx1pbvvagpf4uar w dlkpkkfl9q7udzodqctd451avnwnvqccvtmz0wwj90f8md8mbmrl464wo

Scroll down and choose the subscription period. In our case – it’s a week. Set up the price.  

60fef7a8d11d44258c894e7d 658mke hovj itrvd6mamegcdp6wvefdt0bdamvvssnwg0cca

Usually, you set the price in the basic account currency, and the system converts the price automatically. But you can also edit the price for a specific country manually.  

Please notice that Google shows the tax for every country. It’s great, App Store Connect doesn’t do so.  

60fef7a7ebfe90328d0a6a28 3k q7t5g7luv4axvw25thwlj3aibgsnnkahdtfeleaba31hqwgvo1yin2qayyh1r97tp4p nyvavypbvrt2

Scroll down and choose (if needed): 

  1. Free trial period.
  2. Introductory price, which is an offer for the first payment periods.  
  3. Grace period. If a user has payment issues, you can still provide them with premium access for some number of days.
  4. An opportunity to resubscribe from the Play Store, not from the app, after cancellation. 

Comparing the purchase process in Play Console and App Store Connect

Regardless of the fact that subscriptions are monetized more effectively on iOS, Play Console’s admin board is more convenient, it’s organized and localized better, and it works faster.

The process of product creation is made as simple as possible. Here we told how to create products on iOS.

Getting a list of products in an app

Once the products are created, let’s work on the architecture for accepting and processing purchases. In general, the process looks like this:   

  1. Add a Billing Library.
  2. Develop a class for interaction with products from Google Play.
  3. Implement all methods to process purchases.  
  4. Add server validation of a purchase.  
  5. Collect analytics.

In this part, let’s take a closer look at the first two points.  

Adding Billing Library to a project:

implementation "com.android.billingclient:billing:4.0.0"

At the time of this writing, the latest version is 4.0.0. You can replace it with any other version at any moment.  

Let’s create a wrapper class that will cover the logic of interaction with Google Play and initialize BillingClient from the Billing Library in it. Let’s call this class BillingClientWrapper.

This class will implement PurchasesUpdatedListener interface. We will override a method for it now – onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) – it’s needed right after a purchase is made, but we will describe the implementation process in the next article. 

import android.content.Context
import com.android.billingclient.api.*

class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {

   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()

   override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
       // here come callbacks about new purchases
   }
}

Google recommends to avoid having more than one active connection between BillingClient and Google Play to prevent a callback about a purchase made from being executed several times. Thus, you should have one unique BillingClient in a singleton class. The class in the example isn’t a singleton, but we can use dependency injection (for example, with the help of Dagger or Koin) in this way, allowing only one instance to exist at a single point in time. 

To make any request with Billing Library, BillingClient must have an active connection with Google Play at the moment when the request is being made, but the connection may be lost at some moment. For the sake of convenience, let’s write a wrapper allowing us to make any requests only when the connection is active.   

To get the products, we need their IDs that we set in the market. But it’s not enough for a request, we also need the product type (subscriptions or one-time purchases) that’s why we can get a general list of products by “combining” the results of two requests.   

The request for products is asynchronous, so we need a callback that will either provide us with a list of products or return an error model. When an error occurs, Billing Library returns one of its BillingResponseCodes, as well as debugMessage. Let’s create callback interface and a model for an error:  

interface OnQueryProductsListener {
  fun onSuccess(products: List < SkuDetails > )
  fun onFailure(error: Error)
}

class Error(val responseCode: Int, val debugMessage: String)

Here’s the code for a private method for getting data about a specific type of products and a public method that will “combine” the results of two requests and provide a user with the final list of products or display an error message.  

fun queryProducts(listener: OnQueryProductsListener) {
   val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")

   queryProductsForType(
       skusList,
       BillingClient.SkuType.SUBS
   ) { billingResult, skuDetailsList ->
       if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
           val products = skuDetailsList ?: mutableListOf()
           queryProductsForType(
               skusList,
               BillingClient.SkuType.INAPP
           ) { billingResult, skuDetailsList ->
               if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                   products.addAll(skuDetailsList ?: listOf())
                   listener.onSuccess(products)
               } else {
                   listener.onFailure(
                       Error(billingResult.responseCode, billingResult.debugMessage)
                   )
               }
           }
       } else {
           listener.onFailure(
               Error(billingResult.responseCode, billingResult.debugMessage)
           )
       }
   }
}

private fun queryProductsForType(
   skusList: List<String>,
   @BillingClient.SkuType type: String,
   listener: SkuDetailsResponseListener
) {
   onConnected {
       billingClient.querySkuDetailsAsync(
           SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
           listener
       )
   }
}

Thus, we got valuable information about the products (SkuDetails) where we can see localized names, prices, product type, as well as billing period and information about introductory price and trial period (if it’s available for this user) for subscriptions. Here’s what the final class looks like:   

import android.content.Context
import com.android.billingclient.api.*
class BillingClientWrapper(context: Context) : PurchasesUpdatedListener {
   interface OnQueryProductsListener {
       fun onSuccess(products: List<SkuDetails>)
       fun onFailure(error: Error)
   }
   class Error(val responseCode: Int, val debugMessage: String)
   private val billingClient = BillingClient
       .newBuilder(context)
       .enablePendingPurchases()
       .setListener(this)
       .build()
   fun queryProducts(listener: OnQueryProductsListener) {
       val skusList = listOf("premium_sub_month", "premium_sub_year", "some_inapp")
       queryProductsForType(
           skusList,
           BillingClient.SkuType.SUBS
       ) { billingResult, skuDetailsList ->
           if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
               val products = skuDetailsList ?: mutableListOf()
               queryProductsForType(
                   skusList,
                   BillingClient.SkuType.INAPP
               ) { billingResult, skuDetailsList ->
                   if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                       products.addAll(skuDetailsList ?: listOf())
                       listener.onSuccess(products)
                   } else {
                       listener.onFailure(
                           Error(billingResult.responseCode, billingResult.debugMessage)
                       )
                   }
               }
           } else {
               listener.onFailure(
                   Error(billingResult.responseCode, billingResult.debugMessage)
               )
           }
       }
   }
   private fun queryProductsForType(
       skusList: List<String>,
       @BillingClient.SkuType type: String,
       listener: SkuDetailsResponseListener
   ) {
       onConnected {
           billingClient.querySkuDetailsAsync(
               SkuDetailsParams.newBuilder().setSkusList(skusList).setType(type).build(),
               listener
           )
       }
   }
   private fun onConnected(block: () -> Unit) {
       billingClient.startConnection(object : BillingClientStateListener {
           override fun onBillingSetupFinished(billingResult: BillingResult) {
               block()
           }
           override fun onBillingServiceDisconnected() {
               // Try to restart the connection on the next request to
               // Google Play by calling the startConnection() method.
           }
       })
   }
   override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: MutableList<Purchase>?) {
       // here come callbacks about new purchases
   }
}

That’s all for today. In the next articles, we’re going to tell you about purchase implementation, testing, and error handling.  

Further reading