⟵ Back to blog

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

14 min read

Share

In-app purchases and especially subscriptions are the most popular methods to monetize an app. On the one hand, a subscription allows a developer to develop content and a product, on the other hand, they help a user 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 the topics starting with the creation of in-app purchases and 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.

In this article, we’ll explain how to:  

  • Create a product in Google Play Console;
  • Configure subscriptions: how to specify duration, price, trials; 
  • Get 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 an app.

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

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

  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, as well as analyzing sales statistics will be easier.
  2. The name of the subscription that a user will see in the store.  
  3. Subscription description. A user will see it, too.  

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

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.  

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 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?) {
       // 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 the 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 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 a 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,
   @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?) {
       // 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.  

Vlad Guriev
January 27, 2021