
Trends-insights
Analytics
Money
7 min read
August 4, 2023
Updated: August 21, 2024
13 min read
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.
In this article, we’ll explain how to:
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 subscriptions can be split into several categories:
In this series, we’ll focus on subscriptions mostly.
The whole process of in-app purchase implementation is covered in 5 articles – though right now we’ll focus on part 1 only, make sure to check the rest as well. As you may have already guessed, 5 articles imply that the process of in-app purchase integration is rather complex, so if you don’t want to spend an eternity on coding, I’d recommend checking Adapty as a much quicker and easier way of implementing in-app subscriptions.
Before we start, make sure you:
Now, let’s get down to business and create our first product.
It’s worth mentioning that In-app purchases are subject to 30% commission, but if the user has been subscribed for more than a year or an app earns less than $1М per year, the commission is 15%. Commission fees in this case is nothing peculiar, for example Apple charges the same fee for the iOS apps. It’s just the way huge corporations like Google, Apple, or Microsoft earn their money as businesses. So bear in mind that you won’t be getting 100% of the revenue from your in-app purchases.
Switch to your developer account and choose the app.
Then, in the menu on the left, find the Products section, select Subscriptions and click Create a Subscription.
Then, we’ll see the subscription configurator. Here are some important points.
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):
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.
Once the products are created, let’s work on the architecture for accepting and processing purchases. In general, the process looks like this:
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.
If you are an Android app developer or a product owner looking to escalate your conversion rates and in-app subscriptions, look no further! 🚀 Schedule a free demo call with us today! We’ll guide you through integrating Adapty SDK, an essential tool to maximize your Android in-app subscription revenue swiftly and efficiently. 💰 Don’t miss the opportunity to redefine your app’s success and profitability with Adapty!
Recommended posts
Trends-insights
Analytics
Money
7 min read
August 4, 2023
Product-releases
September 15, 2022