Getting Started With In-App Purchases

Learn how to get started with in-app purchases and implement this library inside your next project. By Mattia Ferigutti.

3.5 (2) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Loading the APK in the Play Console

If you aren’t using the project sample provided in this article, it’s advisable to create a new project and copy all the code and resources appropriately.

To connect the app to the in-app purchases defined in the Play Console, you must load the signed APK, or Android App Bundle, inside the Play Console. For instructions, check out this great article.

Back to the Google Play Developer Console

Now that you built your APK, you can upload it on the Play Console. This part is critical, so follow all the steps carefully.

First, go to All apps. Select Setup and License testing from the menu on the left. Insert all the accounts that can test your apps: These accounts must be associated with Google accounts used in the Play Store.

Then complete all the steps inside LET US KNOW ABOUT THE CONTENT OF YOUR APP. You don’t need to complete the MANAGE HOW YOUR APP IS ORGANIZED AND PRESENTED since you’re just testing the API and the app doesn’t need a page on the Play Store.

Screenshot of the steps required to provide information about your app

Now select Internal testing from the menu on the left. Click Create new release. Upload your app and click Save.

Don’t publish it yet. You’re using Internal Testing because it’s the only one that lets you publish an app without the need to have an app preview.

Then select In-app products from the menu on the left. Click Create product. Set the Product ID, Name, Description and Default price.

You won’t be able to change the Product ID once you assigned it. For the Monsters app, you need to set three different product IDs and their relative prices. Define monster_level_2 as 2 dollars, monster_level_3 as 3 dollars and monster_level_4 as 4 dollars.

Remember to Save and Activate the product.

Screenshot of the Product IDs

Now, go back to Internal testing. Click Testers and Create email list. Every account in this list is eligible to test your app.

Give the list a name, add all the emails and Save changes. Now check the box with your testers list. Scroll down the page and copy the link by clicking Copy link.

Screenshot of the list of testers

Finally, publish the app. Click the Edit release button at the top-right. Then click Reviews release and finally select Start rollout to Internal testing.

Send the copied link to your testers. They need to accept the invite to use in-app purchases. They can also download the app directly from the Google Play Store through this link.

It doesn’t matter if you’re testing the app on your device or emulator. Even if they install the app from Android Studio, they still need to accept the invite. After doing so, they’ll see something like this:

Screenshot of the accepted invite

Good job! Now you’re ready to implement the code to manage the in-app purchases.

Handling Purchases

Open BillingHelper.kt. As you can see, the constructor of the class takes four parameters.

class BillingHelper private constructor(
    application: Application,
    private val defaultScope: CoroutineScope,
    knownInAppSKUs: Array<String>?,
    knowConsumableInAppKUSs: Array<String>?
)

Breaking down the parameters:

  • defaultScope helps execute tasks in background. As you may have noticed, the library you implemented has the ktx abbreviation. This means it supports Kotlin features including coroutines that you’ll use in this tutorial.
  • knownInAppSKUs is a list that contains all the SKUs for non-consumable products.
  • knowConsumableInAppKUSs is a list that contains all the SKUs for consumable products.

This class also implements the Singleton Pattern, which means you can only have one single instance of this class. Having multiple instances, in this case, can lead to errors. For example, imagine if different instances tried to consume the same purchase.

Connecting the Billing Client

It’s time to write some code. Implement PurchasesUpdatedListener and BillingClientStateListener in the class and add all their methods:

override fun onPurchasesUpdated(
  billingResult: BillingResult, 
  list: MutableList<Purchase>?
) {
  
}

override fun onBillingSetupFinished(billingResult: BillingResult) {
  
}

override fun onBillingServiceDisconnected() {
  
}

Every method responds to a specific event:

  • You call onPurchasesUpdated when new purchases occur. This happens in response to a billing flow. You’ll learn about this flow later.
  • onBillingSetupFinished is called to notify you that setup is complete.
  • onBillingServiceDisconnected isn’t usually called and happens primarily if the Google Play Store self-upgrades or is force closed. If this happens, you must reconnect to the billing service.

Once you define the interfaces, initialize the BillingClient inside the init block:

init {

    //Connecting the billing client
    billingClient = BillingClient.newBuilder(application)
        .setListener(this)
        .enablePendingPurchases()
        .build()
    billingClient.startConnection(this)
}

Here’s a code breakdown:

  • Here, you implement enablePendingPurchases() which means you ensure entitlement only once you secure payment. For example, in some countries you can pay for in-app purchases or subscriptions using cash in places authorized by Google. So you need to acknowledge only when you receive the payment.
  • After creating a BillingClient, you need to establish a connection to Google Play by calling startConnection(). The connection process is asynchronous.

Implementing onBillingSetupFinished

When the setup is complete, the BillingClient calls onBillingSetupFinished. You need to check whether the response code from BillingClient has a positive response.

Implement the following code:

override fun onBillingSetupFinished(billingResult: BillingResult) {
    val responseCode = billingResult.responseCode
    val debugMessage = billingResult.debugMessage
    Log.d(TAG, "onBillingSetupFinished: $responseCode $debugMessage")
    when (responseCode) {
      BillingClient.BillingResponseCode.OK -> {
        defaultScope.launch {
          querySkuDetailsAsync()
          restorePurchases()
        }
      }
    }
  }

In this code, you check if responseCode is equal to BillingClient.BillingResponseCode.OK. By doing so, you know that the billing client is ready for you to query purchases, but that doesn’t mean your app is set up correctly in the console. It tells that you have a connection to the Billing service.

You can use the menu on your left to jump to the querySkuDetailsAsync() and restorePurchases() methods in this tutorial.

Adding Flow

You initialized the BillingClient, but you also need to add a purchase flow for every SKU to complete the setup. Each SKU has a different flow which gives you information about the product and notifies you if an event happens.

Inside the BillingHelper class, define addSkuFlows():

private enum class SkuState {
    SKU_STATE_UNPURCHASED, 
    SKU_STATE_PENDING, 
    SKU_STATE_PURCHASED, 
    SKU_STATE_PURCHASED_AND_ACKNOWLEDGED
}

private fun addSkuFlows(skuList: List<String>?) {
    if (null == skuList) {
      Log.e(
        TAG,
        "addSkuFlows: " +
         "SkuList is either null or empty."
      )
    }
    for (sku in skuList!!) {
      val skuState = MutableStateFlow(SkuState.SKU_STATE_UNPURCHASED)
      val details = MutableStateFlow<SkuDetails?>(null)
      // this initialization calls querySkuDetailsAsync() when the first 
      //  subscriber appears
      details.subscriptionCount.map { count ->
        count > 0
      } // map count into active/inactive flag
          .distinctUntilChanged() 
          .onEach { isActive -> // configure an action
            if (isActive) {
              querySkuDetailsAsync()
            }
          }
          .launchIn(defaultScope) // launch it inside defaultScope

      skuStateMap[sku] = skuState
      skuDetailsMap[sku] = details
    }
}

Here’s a code breakdown:

onEach asks for details for each SKU by calling querySkuDetailsAsync(). Finally, it launches this process inside the defaultScope which is a CoroutinesScope passed as parameter of the class.

  • The function takes a list of SKUs as a parameter. It defines a skuState and some details for every SKU. Both instances use the MutableStateFlow class since they only need to hold the latest state of the SKU.
  • SkuState is an enum that defines the states a SKU can have throughout the flow.
  • details implements some operators to manage the data. You use subscriptionCount through the map operator to check if it has any subscribers. distinctUntilChanged() verifies that the new object isn’t the same as the previous one. In this case, it reacts on true and false changes.

    onEach asks for details for each SKU by calling querySkuDetailsAsync(). Finally, it launches this process inside the defaultScope which is a CoroutinesScope passed as parameter of the class.

  • You save both details and skuState inside a map shared with the entire class. This process makies it simple to keep track of all the SKUs.

Now, call this method inside the init block:

init {
    //Add flow for in app purchases
    addSkuFlows(this.knownInAppSKUs)
}