DEV Community

myougaTheAxo
myougaTheAxo

Posted on

In-App Purchase in Android: Google Play Billing Library Guide

In-App Purchase in Android: Google Play Billing Library Guide

Monetizing Android apps through in-app purchases requires integrating Google Play Billing Library. This guide covers setup, product queries, purchase flows, and best practices.

BillingClient Setup

Initialize BillingClient in your Activity or ViewModel:

import com.android.billingclient.api.*

class PurchaseViewModel : ViewModel() {
    private lateinit var billingClient: BillingClient

    fun initBillingClient(context: Context) {
        billingClient = BillingClient.newBuilder(context)
            .setListener(purchasesUpdatedListener)
            .enablePendingPurchases()
            .build()

        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    // Ready to query products
                    queryProducts()
                }
            }

            override fun onBillingServiceDisconnected() {
                // Try to reconnect
            }
        })
    }

    private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.forEach { purchase ->
                    handlePurchase(purchase)
                }
            }
            BillingClient.BillingResponseCode.USER_CANCELED -> {
                // User canceled
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Query Products

Fetch available products (one-time and subscriptions):

fun queryProducts() {
    // One-time products
    val oneTimeProductList = listOf(
        QueryProductDetailsParams.Product(
            productId = "premium_unlock",
            productType = BillingClient.ProductType.INAPP
        )
    )

    val params = QueryProductDetailsParams.newBuilder()
        .setProductList(oneTimeProductList)
        .build()

    billingClient.queryProductDetailsAsync(params) { billingResult, products ->
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            products?.forEach { product ->
                Log.d("Billing", "${product.name}: ${product.oneTimePurchaseOfferDetails?.formattedPrice}")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Launch Billing Flow

Trigger the purchase flow when user clicks "Buy":

fun launchPurchaseFlow(activity: Activity, productDetails: ProductDetails) {
    val billingFlowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(
            listOf(
                BillingFlowParams.ProductDetailsParams.newBuilder()
                    .setProductDetails(productDetails)
                    .build()
            )
        )
        .build()

    billingClient.launchBillingFlow(activity, billingFlowParams)
}
Enter fullscreen mode Exit fullscreen mode

Acknowledge Purchases (Required within 3 days)

After a successful purchase, acknowledge it within 3 days to prevent refund:

private fun handlePurchase(purchase: Purchase) {
    if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
        val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchase.purchaseToken)
            .build()

        billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                Log.d("Billing", "Purchase acknowledged")
                unlockPremiumFeatures()
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Subscriptions with OfferToken

Handle subscription products with offer selection:

fun querySubscriptions() {
    val subscriptionProductList = listOf(
        QueryProductDetailsParams.Product(
            productId = "premium_monthly",
            productType = BillingClient.ProductType.SUBS
        )
    )

    val params = QueryProductDetailsParams.newBuilder()
        .setProductList(subscriptionProductList)
        .build()

    billingClient.queryProductDetailsAsync(params) { billingResult, products ->
        products?.forEach { subscription ->
            subscription.subscriptionOfferDetails?.forEach { offer ->
                Log.d("Billing", "Offer: ${offer.basePlanId}, Price: ${offer.pricingPhases}")
            }
        }
    }
}

fun launchSubscriptionFlow(activity: Activity, productDetails: ProductDetails, offerToken: String) {
    val billingFlowParams = BillingFlowParams.newBuilder()
        .setProductDetailsParamsList(
            listOf(
                BillingFlowParams.ProductDetailsParams.newBuilder()
                    .setProductDetails(productDetails)
                    .setOfferToken(offerToken)
                    .build()
            )
        )
        .build()

    billingClient.launchBillingFlow(activity, billingFlowParams)
}
Enter fullscreen mode Exit fullscreen mode

Purchase Restoration

Restore previous purchases on app reinstall:

fun restorePurchases() {
    billingClient.queryPurchasesAsync(
        QueryPurchasesParams.newBuilder()
            .setProductType(BillingClient.ProductType.INAPP)
            .build()
    ) { billingResult, purchases ->
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
            purchases.forEach { purchase ->
                if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                    unlockPremiumFeatures()
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Compose Purchase UI

Build a purchase UI in Compose that lists products:

@Composable
fun PremiumShop(viewModel: PurchaseViewModel) {
    val products by viewModel.products.collectAsState()

    LazyColumn(modifier = Modifier.fillMaxSize()) {
        items(products) { product ->
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
                    .clickable {
                        viewModel.launchPurchase(product)
                    }
            ) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text(
                        text = product.name,
                        style = MaterialTheme.typography.titleMedium
                    )
                    Text(
                        text = product.description ?: "",
                        style = MaterialTheme.typography.bodySmall
                    )
                    Spacer(modifier = Modifier.height(8.dp))
                    Button(
                        onClick = { viewModel.launchPurchase(product) },
                        modifier = Modifier.align(Alignment.End)
                    ) {
                        Text("Buy for ${product.oneTimePurchaseOfferDetails?.formattedPrice ?: "?"}")
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Acknowledge within 3 days: Google Play can refund unacknowledged purchases after 3 days
  • Test with Test Products: Use Google Play Console's test product IDs (e.g., android.test.purchased)
  • Handle Network Errors: Gracefully retry on connection failures
  • Show Purchase Receipts: Always inform users of successful purchases
  • Restore Purchases: Allow users to restore purchases across devices
  • Encrypt User Data: Never store purchase tokens in plaintext locally

Key Takeaways

  • BillingClient is the official Google library for all purchase flows
  • Three-day acknowledgement requirement is critical to prevent automatic refunds
  • OfferToken enables flexible subscription pricing with trials and base plans
  • Purchase restoration provides seamless user experience across reinstalls
  • Compose makes it easy to build intuitive purchase UIs with reactive state management

${ CTA }

Top comments (0)