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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With In-App Purchases
30 mins
- Getting Started
- Service Fees
- Terminology
- Content Types
- Concepts
- Setting-Up the Google Play Console
- Setting-Up the Android Project
- Loading the APK in the Play Console
- Back to the Google Play Developer Console
- Handling Purchases
- Connecting the Billing Client
- Implementing onBillingSetupFinished
- Adding Flow
- Implementing querySkuDetailsAsync
- Implementing restorePurchases
- Implementing onSkuDetailsResponse
- Implementing launchBillingFlow
- Implementing onPurchasesUpdated
- Implementing handlePurchase
- Implement setSkuStateFromPurchase
- Getting Product Details
- Implementing BillingHelper Inside The Project
- Where to Go From Here?
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.
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.
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.
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:
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 callingstartConnection()
. 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 somedetails
for every SKU. Both instances use theMutableStateFlow
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 usesubscriptionCount
through themap
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 callingquerySkuDetailsAsync()
. Finally, it launches this process inside thedefaultScope
which is aCoroutinesScope
passed as parameter of the class. - You save both
details
andskuState
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)
}