Skip to main content

Implementing In-App Purchases

1. Setting Up Google Play Billing

Example: Basic Setup in AndroidManifest.xml

<uses-permission android:name="com.android.vending.BILLING" />
Permission Description Required
com.android.vending.BILLING Allows app to communicate with Google Play Billing service Yes
Note: Ensure your app is signed with a release key for testing in production.

Example: Adding Play Billing Library

implementation 'com.android.billingclient:billing-ktx:6.2.1'

Setup Steps

  1. Add the billing permission to AndroidManifest.xml
  2. Include the Play Billing library in your build.gradle
  3. Initialize the BillingClient in your activity or fragment
  4. Connect to Google Play Billing service

2. Connecting to BillingClient

Example: Initialize BillingClient

import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams

class BillingManager(private val context: Context) {
    private lateinit var billingClient: BillingClient

    fun initializeBillingClient() {
        billingClient = BillingClient.newBuilder(context)
            .setListener { billingResult, purchases -> handlePurchases(purchases) }
            .enablePendingPurchases()
            .build()
    }

    fun startConnection() {
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponse.OK) {
                    // The BillingClient is ready. You can query purchases here.
                }
            }

            override fun onBillingServiceDisconnected() {
                // Try to reconnect to the billing service.
            }
        })
    }

    private fun handlePurchases(purchases: List<Purchase>?) {
        // Handle purchases here
    }
}
Method Description Return Type
BillingClient.newBuilder(Context) Creates a new BillingClient.Builder instance. BillingClient.Builder
BillingClient.Builder.setListener() Sets the listener for purchase updates. BillingClient.Builder
BillingClient.Builder.enablePendingPurchases() Enables pending purchases for the app. BillingClient.Builder
BillingClient.build() Builds the BillingClient instance. BillingClient
BillingClient.startConnection() Initiates the connection to Google Play Billing service. void
Note: Ensure that the app has internet access and Google Play services are available before attempting to connect.
Warning: The BillingClient must be initialized and connected before querying product details or making purchases.

3. Querying Product Details

Example: Querying Product Details

val productIds = listOf("premium_upgrade", "subscription_monthly")
val queryProductDetails = QueryProductDetailsParams
    .Builder()
    .setProductIds(productIds)
    .build()

billingClient.queryProductDetailsAsync(queryProductDetails) { billingResult, productDetailsList ->
    if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
        for (productDetails in productDetailsList) {
            Log.d("Product", "Title: " + productDetails.title)
            Log.d("Product", "Price: " + productDetails.price)
        }
    } else {
        Log.e("Billing", "Error: " + billingResult.responseCode)
    }
}
Property Description Values
productId Unique identifier for the product string
title Display name of the product string
description Longer description of the product string
price Formatted price string (e.g., "$4.99") string
sku SKU identifier for the product string
productType Type of product (e.g., IN_APP, SUBS) ProductType
Note: Ensure that all product IDs are registered in the Google Play Console before querying.
Warning: Product details may vary based on the user's region or currency settings.

4. Launching Purchase Flow

Property Description Values
skuDetails Product details retrieved from Google Play Billing SkuDetails
activity Current activity context to launch purchase flow Activity
billingClient Instance of BillingClient initialized with startConnection() BillingClient

Example: Launching Purchase Flow

val billingClient = BillingClient.newBuilder(context)
    .setListener { _, _ -> }
    .enablePendingPurchases()
    .build()

billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(billingResult: BillingResult) {
        if (billingResult.responseCode == BillingClient.BillingResponse.OK) {
            val skuDetails = SkuDetails("inapp:product1")
            val purchaseFlow = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build()

            billingClient.launchBillingFlow(activity, purchaseFlow)
        }
    }

    override fun onBillingServiceDisconnected() { }
})
Note: Ensure BillingClient is connected before launching the purchase flow.
Warning: The launchBillingFlow method must be called from the main thread.

5. Processing Purchases

Property Description Values
purchase Represents a purchase object returned by the billing client Purchase
purchaseToken Unique identifier for the purchase transaction String
sku Product identifier (SKU) of the purchased item String
purchaseState Current state of the purchase (e.g., purchased, refunded) PurchaseState

Example: Handling Purchase

class PurchaseHandler : PurchasesUpdatedListener {
    override fun onPurchasesUpdated(
        responseCode: Int,
        purchases: List<Purchase>?
    ) {
        if (responseCode == BillingClient.BillingResponse.OK && purchases != null) {
            for (purchase in purchases) {
                if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                    // Process the purchase
                    val sku = purchase.sku
                    val token = purchase.purchaseToken
                }
            }
        }
    }
}
Note: Always verify the purchase on your backend before granting access to in-app content.
State Description Usage
PURCHASED User has successfully purchased the item Grant access to content
PENDING Transaction is pending confirmation from Google Play Wait for final state before granting access
UNSPECIFIED_STATE Unknown or unexpected state Log and handle appropriately

Example: Verifying Purchase Token

fun verifyPurchase(token: String, sku: String): Boolean {
    // Send token and SKU to your server for verification
    val verificationResult = sendToBackend(token, sku)
    return verificationResult.isValid
}

6. Acknowledging Purchases

After a purchase is successfully processed, it's crucial to acknowledge the purchase on the server to prevent duplicate transactions and ensure proper handling.

Method Description Notes
acknowledgePurchase Acknowledges a purchase for a specific product ID. Must be called within 30 days of purchase to avoid automatic refunds.

Example: Acknowledging a Purchase

val purchase = purchasesResponse.purchasesList.firstOrNull()
if (purchase != null) {
    val acknowledgePurchaseRequest = AcknowledgePurchaseRequest(
        purchaseToken = purchase.purchaseToken,
        productId = purchase.productId
    )
    billingClient.acknowledgePurchase(acknowledgePurchaseRequest) { response ->
        if (response.responseCode == BillingClient.BillingResponseCode.OK) {
            // Purchase acknowledged successfully
        } else {
            // Handle error
        }
    }
}
Note: The acknowledgePurchase method must be called for consumable and non-consumable in-app purchases to confirm that the purchase has been processed correctly.

7. Consuming Consumables

Consumable in-app purchases are items that can be purchased multiple times. They are typically used for virtual currency, game items, or other non-permanent goods.

Property Description Notes
productId Unique identifier for the consumable product Must match the product ID registered in the store
purchaseToken Unique token identifying the purchase Used to consume the purchase on the server or client
purchaseTime Timestamp of the purchase Used for tracking and analytics

Example: Consuming a Consumable

val purchase = purchases.firstOrNull { it.productId == "consumable_item" }
if (purchase != null) {
    val purchaseToken = purchase.purchaseToken
    // Send to backend or consume directly on client
    consumePurchase(purchaseToken)
}
Note: After consuming a consumable, it can be purchased again.
Warning: Always verify purchases on the server to prevent fraud.

Consuming Consumables - Key Points

  • Consumables can be purchased multiple times
  • Each purchase generates a unique purchaseToken
  • Consumables must be consumed to allow re-purchase
  • Server-side verification is highly recommended for security

8. Handling Subscriptions

Property Description Values Notes
sku Unique identifier for the subscription "monthly_subscription" Must match SKU in Google Play Console
type Product type "subs" Indicates subscription product
price Subscription price "$4.99" Formatted as per locale
billingPeriod Subscription billing cycle "P1M" ISO 8601 duration format

Example: Querying Subscription Details

val query = QuerySkuDetailsParams
    .newBuilder()
    .setProductType(BillingClient.ProductType.SUBS)
    .setSkusList(listOf("monthly_subscription"))
    .build()

billingClient.querySkuDetailsAsync(query) { _, detailsList ->
    detailsList?.forEach { detail ->
        Log.d("Subscription", "Title: ${detail.title}, Price: ${detail.price}")
    }
}

Example: Purchasing a Subscription

val purchaseParams = BillingFlowParams
    .newBuilder()
    .setSkuDetails(skuDetails)
    .build()

billingClient.launchBillingFlow(activity, purchaseParams)
Note: Subscriptions must be managed through Google Play Billing Library v5+ for automatic renewal handling.
Warning: Ensure proper handling of subscription state changes to avoid revenue loss or user confusion.
State Description Action Required
ACTIVE User has an active subscription Grant access to premium content
PENDING Subscription is pending confirmation Wait for state change before granting access
EXPIRED Subscription has expired Revoke access and prompt renewal
CANCELLED User cancelled subscription Revoke access immediately

9. Verifying Purchases

Verifying purchases is crucial for ensuring the integrity of in-app transactions and preventing fraud. Android provides mechanisms to verify purchase data through the Google Play Billing Library.

Component Description Notes
Purchase Represents a user's purchase of an in-app product. Includes product ID, purchase token, and purchase time.
PurchaseState Indicates the state of a purchase (e.g., purchased, refunded). Values: PURCHASED, UNSPECIFIED_STATE.
Signature Used to verify the authenticity of purchase data. Generated using the app's private key and the purchase data.

Example: Verifying Purchase Data

import com.android.billingclient.api.Purchase
import java.security.Signature
import java.security.PublicKey

fun verifyPurchase(purchase: Purchase, publicKey: PublicKey): Boolean {
    val signature = Signature.getInstance("SHA256withRSA")
    signature.initVerify(publicKey)
    signature.update(purchase.originalJson.toByteArray())
    return signature.verify(Base64.decode(purchase.signature, Base64.NO_WRAP))
}
Note: Always verify purchases on a secure backend server to prevent tampering.
Warning: Do not rely solely on client-side verification for security-critical operations.
Verification Method Security Level Use Case
verifyPurchase() Client-side Quick checks in app UI
Server-side validation High Secure transaction handling

10. Restoring Purchases

Restoring purchases allows users to recover previously bought in-app items, especially useful after reinstalling an app or switching devices.

Method Description Notes
restorePurchases Initiates the purchase restoration process for a user's previous purchases. Must be called after connecting to the billing client and ensuring the user is authenticated.

Example: Restoring Purchases

val billingClient = BillingClient.newBuilder(context)
    .setListener { _, _ -> }
    .enablePendingPurchases()
    .build()

billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(billingResult: BillingResult) {
        if (billingResult.responseCode == BillingClient.BillingResponse.OK) {
            val restoreRequest = BillingRequest.newBuilder()
                .setProductType(BillingClient.ProductType.IN_APP)
                .build()

            billingClient.acknowledgePurchase(
                AcknowledgePurchaseParams.newBuilder()
                    .setPurchaseToken("purchase_token")
                    .build()
            )
        }
    }

    override fun onBillingServiceDisconnected() {
        // Handle disconnection
    }
})
Note: The restore process requires the user to be signed into their Google account associated with the purchase.
Warning: Ensure that you handle the restore flow securely to prevent unauthorized access or duplication of purchases.

Key Points for Purchase Restoration

  • Use restorePurchases to retrieve past purchases.
  • Always verify the purchase token before granting access to content.
  • Implement proper error handling for network or authentication failures.

Was this article helpful?