Hello! I'm zm soft, a developer who registered in late 2023 and started releasing apps. I'm also planning to release a developer app to help developers work through closed testing together — check it out if you're interested.
Have you implemented in-app purchases in your app yet? As a developer, getting paid for something users genuinely appreciate is one of the most rewarding parts of the process. This post covers how to make that happen.
What Are In-App Purchases?
As you probably know, in-app purchases fall into two categories:
- Consumable items
- Subscription items
Both work by accessing items pre-configured in the Play Store through the Google Play Billing Library, with a portion of the revenue going to Google as a fee. The implementation is similar for both, but subscriptions have their own gotchas — which I'll cover based on my own experience.
Play Store Setup
Registering Items
Before you can display anything, you need to register your items. You can change prices, names, and other details later, so they don't need to be final. The one thing you can't change is the ID — so if you're planning to reuse test items in production, register them with reusable IDs like product_1, 2, 3... or sub_1, 2, 3....
Registration is done under [Monetize] — [In-app products] is for consumables, and [Subscriptions] is for subscription items. Use the [Create xx] button in each section to register items.
The input form is mostly self-explanatory, but I found the price setup for subscriptions confusing at first. I wasn't sure how to set prices for all regions at once. The trick is: [Set prices] → [Country / region] → [Set price].
Clicking that button brings up a dialog that lets you set prices in bulk:
Registering a Test Account
Next, register a test account. Without this, any purchases you make during testing will be real charges — so don't skip this step. Register a test account by setting up a mailing list under [Settings] → [License testing] in the Play Console (the main developer screen after login).
Once your email is registered, going through a purchase flow with that account will show a "test card" in the payment screen, letting you test without actually being charged.
Implementation
On the app side, you'll need to handle:
- Adding the library
- Connecting to the store and fetching item details
- Building the purchase UI
- Triggering the purchase flow
- Handling purchase completion
Adding the Library
You'll need to update two files:
build.gradleAndroidManifest.xml
Example build.gradle update:
dependencies {
implementation "com.android.billingclient:6.0.0"
}
Example AndroidManifest.xml update:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="com.android.vending.BILLING" />
<application
Check the official docs for the latest version numbers.
Connecting to the Store and Fetching Items
Initialize a BillingClient and call startConnection to begin communicating with the store. Use queryProductDetailsAsync to retrieve item details. See the official docs for specifics.
In my case, fetching items in bulk didn't work for some reason, so I first fetched the item list and then used queryProductDetailAsync to get details for each item individually. Whether a retrieved item (SKU) is consumable or a subscription is determined by its type — inapp or subs.
Building the Purchase UI
Once you have the item details, display them in a list so users can select what to buy. When handling subscription items, you need to comply with the Subscription Policy. In my case, the app was rejected twice in a row for:
- Incomplete localization of price and terms
- Missing offer condition descriptions
On the localization issue: the problem was with how the billing period was displayed (e.g., "$10/month"). When the app ran in a locale for a language I hadn't localized, the word "month" wasn't translated. The price itself is fine — when fetched via BillingClient, you get formattedPrice as a string that includes currency information, so no translation is needed. But the period comes back in ISO 8601 format (e.g., P1M), and you have to translate that yourself. I handled it with per-language conversion like this:
fun formatBillingPeriod(billingPeriod: String, languageCode: String): String {
return when(languageCode) {
"en" -> {
when (billingPeriod) {
"P1W" -> "weekly"
"P1M" -> "monthly"
"P3M" -> "every 3 months"
"P6M" -> "every 6 months"
"P1Y" -> "annually"
else -> "unknown"
}
}
"ja" -> {
when (billingPeriod) {
"P1W" -> "週間"
"P1M" -> "月額"
"P3M" -> "3ヶ月ごと"
"P6M" -> "6ヶ月ごと"
"P1Y" -> "年額"
else -> "不明"
}
}
"fr" -> {
when (billingPeriod) {
"P1W" -> "hebdomadaire"
"P1M" -> "mensuel"
"P3M" -> "tous les 3 mois"
"P6M" -> "tous les 6 mois"
"P1Y" -> "annuel"
else -> "inconnu"
}
}
"es" -> {
when (billingPeriod) {
"P1W" -> "semanal"
"P1M" -> "mensual"
"P3M" -> "cada 3 meses"
"P6M" -> "cada 6 meses"
"P1Y" -> "anual"
else -> "desconocido"
}
}
"de" -> {
when (billingPeriod) {
"P1W" -> "wöchentlich"
"P1M" -> "monatlich"
"P3M" -> "alle 3 Monate"
"P6M" -> "alle 6 Monate"
"P1Y" -> "jährlich"
else -> "unbekannt"
}
}
else -> "unknown"
}
}
On the offer conditions issue:
I understood the problem, but wasn't sure exactly what wording to use. Here's an excerpt from the actual rejection email:
Issue found: Violation of Subscriptions policy
Your app does not comply with the Subscriptions policy.
- Your offer does not clearly and accurately describe the terms of your trial offer or introductory pricing, including when a free trial will convert to a paid subscription, how much the paid subscription will cost, and that a user can cancel if they do not want to convert to a paid subscription.
My solution was to look at how other apps handle it and borrow their wording. The language is fairly boilerplate, so following an established precedent seemed smarter than reinventing it.
Triggering the Purchase Flow
Once users select an item from your UI, launch the purchase flow for that item to display the actual purchase screen.
Testing
Log in with the test account you set up earlier to run through purchases without incurring actual charges. Two things to keep in mind:
- The purchase flow must run on the store-registered app build (not a debug build)
- Subscription renewal intervals are set to custom short durations during testing
Debug builds will show an error on the purchase screen and won't complete the flow.
For subscriptions, when you proceed to the purchase screen, it will show the test card. But the renewal interval shown on the screen won't match what you actually configured — mine showed 5 minutes. It seems subscriptions are always shown with an extremely short fixed interval for testing purposes. (I spent a while thinking I'd misconfigured something.)
Summary
In-app purchases look intimidating at first, but once you understand the overall picture, the implementation itself is surprisingly straightforward using the official documentation. Subscriptions do have some hidden gotchas that you won't know about until you run into them — hopefully the issues I hit save you some time.