MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Monetizing with in-app purchases requires more than integrating the Play Billing Library. Here's how to handle purchase flows, verify purchases securely, restore purchases, and avoid the common mistakes that cause rejected apps and failed payments.
On this page
IAP implementation has real consequences: failed payments mean lost revenue, unverified purchases mean fraud, and unclaimed entitlements mean angry users. Here's how to do it correctly.
// build.gradle.kts
implementation("com.android.billingclient:billing-ktx:7.0.0")The Billing Library is Google's official client. Don't use third-party billing wrappers — they add complexity and lag behind API updates.
class BillingManager(private val context: Context) {
private val billingClient = BillingClient.newBuilder(context)
.setListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
purchases?.forEach { processPurchase(it) }
}
}
.enablePendingPurchases()
.build()
suspend fun connect(): Boolean = suspendCancellableCoroutine { continuation ->
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
continuation.resume(result.responseCode == BillingClient.BillingResponseCode.OK)
}
override fun onBillingServiceDisconnected() {
continuation.resume(false)
}
})
}
}The billing client must be connected before any purchases. Handle disconnection — reconnect before any billing operation.
suspend fun getProducts(productIds: List<String>): List<ProductDetails> {
if (!billingClient.isReady) connect()
val params = QueryProductDetailsParams.newBuilder()
.setProductList(
productIds.map { productId ->
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.INAPP) // or SUBS for subscriptions
.build()
}
)
.build()
val result = billingClient.queryProductDetails(params)
return if (result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
result.productDetailsList ?: emptyList()
} else {
emptyList()
}
}Always show the price from
ProductDetailsval price = productDetails.oneTimePurchaseOfferDetails?.formattedPrice
// Displays "₹500.00" or "$4.99" based on user's localefun launchPurchaseFlow(activity: Activity, productDetails: ProductDetails) {
val offerToken = productDetails.subscriptionOfferDetails?.get(0)?.offerToken
val productDetailsParamsList = listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.apply {
offerToken?.let { setOfferToken(it) } // Required for subscriptions
}
.build()
)
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build()
val result = billingClient.launchBillingFlow(activity, billingFlowParams)
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
// Handle launch failure
}
}This is the most critical part. You must acknowledge purchases or Google will refund them within 3 days:
private fun processPurchase(purchase: Purchase) {
if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) return
// 1. Verify the purchase server-side BEFORE granting entitlement
verifyPurchaseWithServer(purchase) { isValid ->
if (isValid) {
// 2. Grant entitlement
grantPremiumAccess(purchase.products)
// 3. Acknowledge the purchase (required within 3 days)
if (!purchase.isAcknowledged) {
acknowledgePurchase(purchase.purchaseToken)
}
}
}
}
private fun acknowledgePurchase(purchaseToken: String) {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { result ->
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
// Retry — this must succeed or Google will refund
scheduleAcknowledgeRetry(purchaseToken)
}
}
}[!IMPORTANT] Never grant entitlement without server-side verification of the purchase token. The Play Billing Library doesn't prevent fake purchase receipts. Always verify the
against Google's servers before granting access.codepurchaseToken
Your backend verifies with the Google Play Developer API:
GET https://www.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}A valid response includes
purchaseState: 0For subscriptions:
GET https://www.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}Users reinstalling your app should regain their purchases. Query existing purchases on app launch:
suspend fun restorePurchases() {
if (!billingClient.isReady) connect()
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
val result = billingClient.queryPurchasesAsync(params)
result.purchasesList.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
processPurchase(purchase) // Grant entitlement if not already active
}
}
}Call
restorePurchases()Granting entitlement before verification. Fast-path to fraud.
Not handling PENDING
PENDINGNot acknowledging purchases promptly. Google refunds unacknowledged purchases after 3 days. Implement retry logic.
Not restoring purchases on launch. Users assume their paid features persist. Silently restoring them is correct UX.
Hardcoding prices. Always use
formattedPriceProductDetailsPENDINGProductDetails.formattedPriceSudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read