An Android Studio In-App Purchasing Tutorial

In the previous chapter, we explored how to integrate in-app purchasing into an Android project and also looked at some code samples that can be used when working on your own projects. This chapter will put this theory into practice by creating an example project demonstrating how to add a consumable in-app product to an Android app. The tutorial will also show how in-app products are added and managed within the Google Play Console and explain how to enable test payments to make purchases during testing without having to spend real money.

About the In-App Purchasing Example Project

The simple concept behind this project is an app in which an in-app product must be purchased before a button can be clicked. This in-app product is consumed each time the button is clicked, requiring the user to repurchase the product each time they want to be able to click the button. On initialization, the app will connect to the app store, obtain product details, and display the product name. Once the app has established that the product is available, a purchase button will be enabled, which will step through the purchase process when clicked. Upon completion of the purchase, a second button will be enabled so the user can click on it and consume the purchase.

Creating the InAppPurchase Project

The first step in this exercise is to create a new project. Launch Android Studio and select the New Project option from the welcome screen. Choose the Empty Views Activity template in the new project dialog before clicking the Next button.

Enter InAppPurchase into the Name field and specify a package name that uniquely identifies your app within the Google Play ecosystem (for example, com.<your company>.InAppPurchase). Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin. Once the project has been created, use the steps outlined in section 18.8 Migrating a Project to View Binding to convert the project to use view binding.

Adding Libraries to the Project

Before writing code, some libraries must be added to the project build configuration, including the standard Android billing client library. Later in the project, we will also need to use the ImmutableList class, part of Google’s Guava Core Java libraries. Add these libraries now by modifying the Gradle Scripts -> build.gradle.kts (Module: app) file with the following changes:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

.
.
dependencies {
.
.
    implementation ("com.android.billingclient:billing:6.0.1")
    implementation ("com.android.billingclient:billing-ktx:6.0.1")
    implementation ("com.google.guava:guava:24.1-jre")
    implementation ("com.google.guava:guava:27.0.1-android")
.
.Code language: Gradle (gradle)

Click on the Sync Now link at the top of the editor panel to commit these changes.

Designing the User Interface

The user interface will consist of the existing TextView and two Buttons. With the activity_main.xml file loaded into the editor, drag and drop two Button views onto the layout so that one is above and the other below the TextView. Select the TextView and change the id attribute to statusText.

Click on the Clear all Constraints button in the toolbar and shift-click to select all three views. Right-click on the top-most Button view and select the Center -> Horizontally in Parent menu option. Repeat this step once more, selecting Chains -> Create Vertical Chain. Change the text attribute of the top button so that it reads “Consume Purchase” and the id to consumeButton. Also, configure the onClick property to call a method named consumePurchase.

Select the bottom-most button and repeat the above steps, setting the text to “Buy Product”, the id to buyButton, and the onClick callback to makePurchase. Once completed, the layout should match that shown in Figure 88-1:

Figure 88-1

Adding the App to the Google Play Store

Using the steps outlined in the chapter entitled “Creating, Testing and Uploading an Android App Bundle”, sign into the Play Console, create a new app, and set up a new internal testing track, including the email addresses of designated testers. Return to Android Studio and generate a signed release app bundle for the project. Once the bundle file has been generated, upload it to the internal testing track and roll it out for testing.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Now that the app is in the Google Play Store, we are ready to create an in-app product for the project.

Creating an In-App Product

With the app selected in the Play Console, scroll down the list of options in the left-hand panel until the Monetize section appears. Within this section, select the In-app products option listed under Products, as shown in Figure 88-2:

Figure 88-2

On the In-app products page, click on the Create product button:

Figure 88-3

On the new product screen, enter the following information before saving the new product:

  • Product ID: one_button_click
  • Name: A Button Click
  • Description: This is a test in-app product that allows a button to be clicked once. • Default price: Set to the lowest possible price in your preferred currency.

Enabling License Testers

When testing in-app billing, it is useful to make test purchases without spending any money. This can be achieved by enabling license testing for the internal track testers. License testers can use a test payment card when making purchases so that they are not charged.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Within the Play Console, return to the main home screen and select the Setup -> License testing option:

Figure 88-4

Within the license testing screen, add the testers that were added for the internal testing track, change the License response setting to RESPOND_NORMALLY, and save the changes:

Figure 88-5

Now that the app and the in-app product have been set up in the Play Console, we can add code to the project.

Initializing the Billing Client

Edit the MainActivity.kt file and make the following changes to begin implementing the in-app purchase functionality:

.
.
import android.util.Log
import com.android.billingclient.api.*
.
.
class MainActivity : AppCompatActivity() {
 
    private lateinit var binding: ActivityMainBinding
    private lateinit var billingClient: BillingClient
    private lateinit var productDetails: ProductDetails
    private lateinit var purchase: Purchase
    private val demo_product = "one_button_click"
 
    val TAG = "InAppPurchaseTag"
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        billingSetup()
    }
 
    private fun billingSetup() {
        billingClient = BillingClient.newBuilder(this)
            .setListener(purchasesUpdatedListener)
            .enablePendingPurchases()
            .build()
 
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(
                billingResult: BillingResult
            ) {
                if (billingResult.responseCode ==
                    BillingClient.BillingResponseCode.OK
                ) {
                    Log.i(TAG, "OnBillingSetupFinish connected")
                    queryProduct(demo_product)
                } else {
                    Log.i(TAG, "OnBillingSetupFinish failed")
                }
            }
 
            override fun onBillingServiceDisconnected() {
                Log.i(TAG, "OnBillingSetupFinish connection lost")
            }
        })
    }
.
.Code language: Kotlin (kotlin)

When the app starts, the onCreate() method will now call billingSetup(), which will, in turn, create a new billing client instance and attempt to connect to the Google Play Billing Library. The onBillingSetupFinished() listener will be called when the connection attempt completes and output Logcat messages indicating the success or otherwise of the connection attempt. Finally, we have also implemented the onBillingServiceDisconnected() callback which will be called if the Google Play Billing Library connection is lost.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

If the connection is successful, queryProduct() is called. This method and the purchasesUpdatedListener assigned to the billing client must be added.

Querying the Product

To make sure the product is available for purchase, we need to create a QueryProductDetailsParams instance configured with the product ID that was specified in the Play Console and pass it to the queryProductDetailsAsync() method of the billing client. This will require that we also add the onProductDetailsResponse() callback method, where we will check that the product exists, extract the product name, and display it on the status TextView. Now that we have obtained the product details, we can also safely enable the buy button. Within the MainActivity.kt file, add the queryProduct() method so that it reads as follows:

.
.
import com.android.billingclient.api.QueryProductDetailsParams.Product
import com.google.common.collect.ImmutableList
.
.
private fun queryProduct(productId: String) {
    val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
        .setProductList(
            ImmutableList.of(
                Product.newBuilder()
                    .setProductId(productId)
                    .setProductType(
                        BillingClient.ProductType.INAPP
                    )
                    .build()
            )
        )
        .build()
 
    billingClient.queryProductDetailsAsync(
        queryProductDetailsParams
    ) { billingResult, productDetailsList ->
        if (productDetailsList.isNotEmpty()) {
            productDetails = productDetailsList[0]
            runOnUiThread {
                binding.statusText.text = productDetails.name
            }
        } else {
            Log.i(TAG, "onProductDetailsResponse: No products")
        }
    }
}Code language: Kotlin (kotlin)

Much of the code used here should be familiar from the previous chapter. The listener code checks that at least one product matches the query criteria. The ProductDetails object is then extracted from the first matching product, stored in the productDetails variable, and the product name property is displayed on the TextView. One point of note is that when we display the product name on the status TextView, we do so by calling runOnUiThread(). This is necessary because the listener is not running on the main thread, so it cannot safely make direct changes to the user interface. The runOnUiThread() method provides a quick and convenient way to execute code on the main thread without using coroutines.

Launching the Purchase Flow

When the user clicks the buy button, makePurchase() will be called to start the purchase process. We can now add this method as follows:

.
.
import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams
import android.view.View
.
.
fun makePurchase(view: View?) {
    val billingFlowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(
            ImmutableList.of(
                ProductDetailsParams.newBuilder()
                    .setProductDetails(productDetails)
                    .build()
            )
        )
        .build()
 
    billingClient.launchBillingFlow(this, billingFlowParams)
}Code language: Kotlin (kotlin)

Handling Purchase Updates

The results of the purchase process will be reported to the app via the PurchaseUpdatedListener assigned to the billing client during the initialization phase. Add this handler now as follows:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

private val purchasesUpdatedListener =
    PurchasesUpdatedListener { billingResult, purchases ->
        if (billingResult.responseCode ==
            BillingClient.BillingResponseCode.OK
            && purchases != null
        ) {
            for (purchase in purchases) {
                completePurchase(purchase)
            }
        } else if (billingResult.responseCode ==
            BillingClient.BillingResponseCode.USER_CANCELED
        ) {
            Log.i(TAG, "onPurchasesUpdated: Purchase Canceled")
        } else {
            Log.i(TAG, "onPurchasesUpdated: Error")
        }
    }Code language: Kotlin (kotlin)

The handler will output log messages if the user cancels the purchase or another error occurs. However, a successful purchase results in a call to the completePurchase() method, which is passed the current Purchase object. Add this method as outlined below:

private fun completePurchase(item: Purchase) {
    purchase = item
    if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
        runOnUiThread {
            binding.consumeButton.isEnabled = true
            binding.statusText.text = "Purchase Complete"
            binding.buyButton.isEnabled = false
        }
    }
}Code language: Kotlin (kotlin)

This method stores the purchase before verifying that the product has indeed been purchased and that payment is not still pending. The “consume” button is enabled, and the user is notified of the successful purchase. The buy button is also disabled to prevent the user from repurchasing before consuming the purchase.

Consuming the Product

With the user now able to click on the “consume” button, the next step is to ensure the product is consumed so that only one click can be performed before another button click is purchased. This requires that we now write the consumePurchase() method:

.
.
import kotlinx.coroutines.*
.
.
class MainActivity : AppCompatActivity() {
 
    private val coroutineScope = CoroutineScope(Dispatchers.IO)
.
.
    fun consumePurchase(view: View?) {
 
        val consumeParams = ConsumeParams.newBuilder()
            .setPurchaseToken(purchase.purchaseToken)
            .build()
 
         coroutineScope.launch {
             val result = billingClient.consumePurchase(consumeParams) 
             if (result.billingResult.responseCode == 
                      BillingClient.BillingResponseCode.OK) {
                 runOnUiThread() {
                     binding.consumeButton.isEnabled = false
                     binding.statusText.text = "Purchase consumed"
                     binding.buyButton.isEnabled = true
                 }
             }
         }
    }
.
.Code language: Kotlin (kotlin)

This method creates a ConsumeParams instance and configures it with the purchase token for the current purchase (obtained from the Purchase object previously saved in the completePurchase() method). This is passed to the consumePurchase() method, which is launched within a coroutine using the IO dispatcher. If the product is successfully consumed, code is executed in the main thread to disable the consume button, enable the buy button, and update the status text.

Restoring a Previous Purchase

With the code added so far, we can purchase and consume a product within a single session. If we were to make a purchase and exit the app before consuming it, the purchase would be lost when the app restarts. We can solve this problem by configuring a QueryPurchasesParams instance to search for the unconsumed In-App product and passing it to the queryPurchasesAsync() method of the billing client together with a reference to a listener that will be called with the results. Add a new function and the listener to the MainActivity.kt file as follows:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

private fun reloadPurchase() {
 
    val queryPurchasesParams = QueryPurchasesParams.newBuilder()
        .setProductType(BillingClient.ProductType.INAPP)
        .build()
 
    billingClient.queryPurchasesAsync(
        queryPurchasesParams,
            purchasesListener
    )
}
 
private val purchasesListener =
    PurchasesResponseListener { billingResult, purchases ->
        if (purchases.isNotEmpty()) {
            purchase = purchases.first()
            binding.consumeButton.isEnabled = true
            binding.buyButton.isEnabled = false
        } else {
            binding.consumeButton.isEnabled = false
        }
    }Code language: Kotlin (kotlin)

If the list of purchases passed to the listener is not empty, the first purchase in the list is assigned to the purchase variable, and the consume button is enabled (in a more complete implementation, code should be added to check this is the correct product by comparing the product id and to handle the return of multiple purchases). If no purchases are found, the consume button is disabled until another purchase is made. All that remains is to call our new reloadPurchase() method during the billing setup process as follows:

private fun billingSetup() {
.
.
            if (billingResult.responseCode ==
                BillingClient.BillingResponseCode.OK
            ) {
                Log.i(TAG, "OnBillingSetupFinish connected")
                queryProduct(demo_product)
                reloadPurchase()
            } else {
                Log.i(TAG, "OnBillingSetupFinish failed")
            }
.
.
}Code language: Kotlin (kotlin)

Testing the App

Before we can test the app, we need to upload this latest version to the Play Console. As we already have version 1 uploaded, we need to increase the version number in the build.gradle.kts (Module: app) file:

.
.
defaultConfig {
    applicationId "com.ebookfrenzy.inapppurchase"
    minSdk 26
    targetSdk 32
    versionCode 2
    versionName "2.0"
.
.Code language: Gradle (gradle)

Sync the build configuration, then follow the steps in the Creating, Testing, and Uploading an Android App Bundle chapter to generate a new app bundle, upload it to the internal test track, and roll it out to the testers. Next, using the internal testing link, install the app on a device or emulator where one of the test accounts is signed in. To locate the testing link, select the app in the Google Play Console and choose the Internal testing option from the navigation panel followed by the Testers tab, as shown in Figure 88-6:

Figure 88-6

Scroll to the “How testers join your test” section of the screen and click on Copy link:

Figure 88-7

Open the Chrome browser on the testing device or emulator, enter the testing link, and follow the instructions to install the app from the Play Store. After the app starts, it should, after a short delay, display the product name on the TextView. Clicking the buy button will begin the purchase flow, as shown in Figure 88-8:

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Figure 88-8

Tap the buy button to complete the purchase using the test card and wait for the Consume Purchase button to be enabled. Before tapping this button, attempt to purchase the product again and verify that it is not possible because you already own the product.

Tap the Consume Purchase button and wait for the “Purchase consumed” message to appear on the TextView. With the product consumed, it should now be possible to purchase it again. Make another purchase, then terminate and restart the app. The app should locate the previous unconsumed purchase and enable the consume button.

Troubleshooting

If you encounter problems with the purchase, make sure the device is attached to Android Studio via a USB cable or WiFi, and select it from within the Logcat panel. Enter InAppPurchaseTag into the Logcat search bar and check the diagnostic output, adding additional Log calls in the code if necessary. For additional information about failures, a useful trick is to access the debug message from BillingResult instances, for example:

.
.
} else if (billingResult.responseCode ==
    BillingClient.BillingResponseCode.USER_CANCELED
) {
    Log.i(TAG, "onPurchasesUpdated: Purchase Canceled")
} else {
    Log.i(TAG, billingResult.getDebugMessage())
}Code language: Kotlin (kotlin)

Note that as long as you leave the app version number unchanged in the module-level build.gradle.kts file, you should now be able to run modified versions of the app directly on the device or emulator without having to re-bundle and upload it to the console.

If the test payment card is not listed, ensure the device user account has been added to the license testers list. If the app is running on a physical device, try it on an emulator. If all else fails, you can enter a valid payment method to make test purchases and then refund yourself using the Order Management screen accessible from the Play Console home page.

 

You are reading a sample chapter from Android Studio Giraffe Essentials – Kotlin Edition.

Buy the full book now in eBook (PDF) or Print format.

The full book contains 94 chapters and over 830 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Summary

In this chapter, we created a project demonstrating adding an in-app product to an Android app. This included the creation of the product within the Google Play Console and writing code to initialize and connect to the billing client, querying available products, and purchasing and consuming the product. We also explained how to add license testers using the Play Console to make purchases during testing without spending money.