# Google Play Billing Library v7: Android Subscription 2026
Google Play Billing Library v7 (2024 Q4 GA, 2026'da mature) Android IAP'ı modernleştirdi. Kotlin DSL, Product details API (subscription offers flexibility), installment plans (yeni), Real-time Developer Notifications (RTDN) via Pub/Sub — hepsi production'da. Bu rehber v7'nin yeniliklerini, Compose UI ile paywall implementation'ı, server-side receipt validation ve test stratejisini kapsar.
💡 Pro Tip: Play Billing v6'dan v7'ye migration breaking — querySkuDetailsAsync removed, artık ProductDetails. Tek attempt'te rewrite etme, v7 parallel implement edip A/B test et.İçindekiler
- v7 vs v6: Neler Değişti
- BillingClient Setup (Kotlin DSL)
- Product Details API
- Purchase Flow
- Subscription Offers + Installment Plans
- RTDN: Real-time Developer Notifications
- Server-Side Receipt Validation
- Compose UI Paywall
- Testing ve License Tester
v7 vs v6: Neler Değişti
Özellik | v6 | v7 |
|---|---|---|
Subscription plans | 1 base plan | Multiple plans per subscription |
Installment plans | - | Yes (yearly → monthly breakdown) |
Product API | SkuDetails (deprecated) | ProductDetails |
Kotlin support | Partial | Kotlin DSL + Flow |
RTDN support | webhook only | Pub/Sub + webhook |
Offer tags | Limited | Advanced tag-based targeting |
Min Android | API 21 | API 21 |
BillingClient Setup (Kotlin DSL)
kotlin
1import com.android.billingclient.api.*2 3class BillingManager(context: Context) : BillingClientStateListener {4 val purchasesUpdatedListener = PurchasesUpdatedListener { result, purchases ->5 // Purchase events handler6 }7 8 val billingClient: BillingClient = BillingClient.newBuilder(context)9 .setListener(purchasesUpdatedListener)10 .enablePendingPurchases(11 PendingPurchasesParams.newBuilder()12 .enableOneTimeProducts()13 .enablePrepaidPlans()14 .build()15 )16 .build()17 18 init {19 billingClient.startConnection(this)20 }21 22 override fun onBillingSetupFinished(result: BillingResult) {23 if (result.responseCode == BillingClient.BillingResponseCode.OK) {24 // Ready to query products25 }26 }27 28 override fun onBillingServiceDisconnected() {29 // Retry logic with exponential backoff30 }31}Product Details API
kotlin
1suspend fun queryProducts(): List { 2 val params = QueryProductDetailsParams.newBuilder()3 .setProductList(listOf(4 QueryProductDetailsParams.Product.newBuilder()5 .setProductId("pro_monthly")6 .setProductType(BillingClient.ProductType.SUBS)7 .build(),8 QueryProductDetailsParams.Product.newBuilder()9 .setProductId("coin_100")10 .setProductType(BillingClient.ProductType.INAPP)11 .build(),12 ))13 .build()14 15 return withContext(Dispatchers.IO) {16 val result = billingClient.queryProductDetails(params)17 result.productDetailsList ?: emptyList()18 }19}ProductDetails Structure
kotlin
1data class ProductDetails(2 val productId: String,3 val title: String,4 val description: String,5 val productType: String, // "subs" or "inapp"6 val subscriptionOfferDetails: List?, 7 val oneTimePurchaseOfferDetails: OneTimePurchaseOfferDetails?8)9 10data class SubscriptionOfferDetails(11 val offerToken: String, // Launch için token12 val basePlanId: String,13 val offerId: String?,14 val offerTags: List, // ["discount", "winback"] 15 val pricingPhases: List // Multiple phases (trial → paid) 16)Purchase Flow
kotlin
1suspend fun launchPurchase(2 activity: Activity,3 productDetails: ProductDetails,4 offerToken: String? = null5) {6 val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()7 .setProductDetails(productDetails)8 9 // Subscription için offerToken gerekli10 offerToken?.let { productDetailsParams.setOfferToken(it) }11 12 val flowParams = BillingFlowParams.newBuilder()13 .setProductDetailsParamsList(listOf(productDetailsParams.build()))14 .build()15 16 val result = billingClient.launchBillingFlow(activity, flowParams)17 if (result.responseCode != BillingClient.BillingResponseCode.OK) {18 // Handle error19 }20}21 22private fun handlePurchase(purchase: Purchase) = lifecycleScope.launch {23 if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) return@launch24 25 // 1. Server-side validation26 val valid = validateOnServer(purchase)27 if (!valid) return@launch28 29 // 2. Acknowledge (subscriptions) veya consume (consumables)30 if (purchase.products.first().startsWith("coin_")) {31 val consumeParams = ConsumeParams.newBuilder()32 .setPurchaseToken(purchase.purchaseToken)33 .build()34 billingClient.consumePurchase(consumeParams)35 } else {36 if (!purchase.isAcknowledged) {37 val ackParams = AcknowledgePurchaseParams.newBuilder()38 .setPurchaseToken(purchase.purchaseToken)39 .build()40 billingClient.acknowledgePurchase(ackParams)41 }42 }43 44 // 3. Grant entitlement45 UserRepository.grantPro(purchase.purchaseToken)46}Subscription Offers + Installment Plans
Multiple Base Plans
Bir subscription product'ın multiple base plan'ları olabilir:
kotlin
1// "pro" subscription ürünü2// Base plans:3// - monthly ($9.99)4// - yearly ($99.99, %17 indirim)5// - installment (yearly $99.99, monthly $8.33 × 12)6 7productDetails.subscriptionOfferDetails?.forEach { offer ->8 println("Base plan: ${offer.basePlanId}")9 println("Tags: ${offer.offerTags}")10 println("Pricing phases:")11 offer.pricingPhases.forEach { phase ->12 println(" - ${phase.billingPeriod} : ${phase.formattedPrice}")13 }14}Installment Plans (Yeni)
2024'te Google Play'e eklenen özellik. Yıllık abonelik aylık taksitlerle ödenebiliyor:
kotlin
1// User'a seçenek sun:2val yearlyOffer = offers.first { it.basePlanId == "yearly" }3val installmentOffer = offers.first { it.basePlanId == "yearly_installment" }4 5Text("Yıllık $99.99 (tek ödeme)")6Text("Yıllık, aylık $8.33 × 12 ödeme") // installmentWinback Offer
Canceled user'lara tekrar subscribe için özel fiyat:
kotlin
1val winbackOffer = offers.firstOrNull { "winback" in it.offerTags }2winbackOffer?.let {3 // "Geri dön, ilk 3 ay $2.99" UI göster4 launchPurchase(activity, productDetails, it.offerToken)5}RTDN: Real-time Developer Notifications
v7 ile Pub/Sub native support. Google Cloud Pub/Sub'a subscribe ol, subscription event'leri anlık gelir.
Pub/Sub Setup
bash
1# Google Cloud Console'dan Pub/Sub topic oluştur2gcloud pubsub topics create play-rtdn3 4# Service account ile subscribe5gcloud pubsub subscriptions create my-backend-sub \6 --topic play-rtdn \7 --push-endpoint https://myapp.com/api/play-rtdnPlay Console'da Enable
App > Monetize > Subscriptions > "Real-time developer notifications" → topic name gir.
Backend Handler
typescript
1app.post('/api/play-rtdn', async (req, res) => {2 const message = req.body.message;3 const data = JSON.parse(Buffer.from(message.data, 'base64').toString());4 5 // data.subscriptionNotification.notificationType6 switch (data.subscriptionNotification.notificationType) {7 case 1: // SUBSCRIPTION_RECOVERED8 case 2: // SUBSCRIPTION_RENEWED9 await grantPro(data.subscriptionNotification.purchaseToken);10 break;11 case 3: // SUBSCRIPTION_CANCELED12 await markCanceled(data.subscriptionNotification.purchaseToken);13 break;14 case 12: // SUBSCRIPTION_REVOKED (refund)15 await revokePro(data.subscriptionNotification.purchaseToken);16 break;17 case 13: // SUBSCRIPTION_EXPIRED18 await expirePro(data.subscriptionNotification.purchaseToken);19 break;20 }21 22 res.status(200).send('OK');23});Server-Side Receipt Validation
Google Play Developer API ile validate:
typescript
1import { google } from 'googleapis';2 3async function validatePurchase(4 packageName: string,5 productId: string,6 purchaseToken: string7): Promise { 8 const auth = new google.auth.GoogleAuth({9 keyFile: 'service-account.json',10 scopes: ['https://www.googleapis.com/auth/androidpublisher']11 });12 13 const androidpublisher = google.androidpublisher({14 version: 'v3',15 auth: await auth.getClient()16 });17 18 const result = await androidpublisher.purchases.subscriptionsv2.get({19 packageName,20 token: purchaseToken21 });22 23 return {24 valid: result.data.subscriptionState === 'SUBSCRIPTION_STATE_ACTIVE',25 expiryTime: new Date(result.data.lineItems[0].expiryTime!),26 autoRenewing: result.data.lineItems[0].autoRenewingPlan?.autoRenewEnabled27 };28}Compose UI Paywall
kotlin
1@Composable2fun PaywallScreen(3 vm: BillingViewModel = hiltViewModel(),4 onDismiss: () -> Unit5) {6 val products by vm.products.collectAsState()7 val selected by vm.selectedProduct.collectAsState()8 val isLoading by vm.isLoading.collectAsState()9 10 Column(11 modifier = Modifier12 .fillMaxSize()13 .padding(16.dp),14 verticalArrangement = Arrangement.spacedBy(12.dp)15 ) {16 Text(17 "Pro'ya Yükselt",18 style = MaterialTheme.typography.headlineLarge19 )20 21 products.forEach { product ->22 ProductCard(23 product = product,24 isSelected = selected == product,25 onClick = { vm.select(product) }26 )27 }28 29 val activity = LocalContext.current as Activity30 Button(31 onClick = { selected?.let { vm.purchase(activity, it) } },32 enabled = selected != null && !isLoading,33 modifier = Modifier.fillMaxWidth()34 ) {35 if (isLoading) CircularProgressIndicator(modifier = Modifier.size(24.dp))36 else Text("Satın Al")37 }38 39 TextButton(onClick = onDismiss) {40 Text("Şimdi değil")41 }42 }43}Testing ve License Tester
License Tester Setup
Play Console > Testing > License testers > Gmail ekle.
Bu Gmail ile signed APK install edilince:
- Real transaction olmaz (refund anında)
- Subscription period 5 dk (günlük yerine)
- Renewal 5 dk'da bir test edilir
Static Response Testing
Test için response code simulation:
kotlin
1// android.test.purchased — simulated purchase2// android.test.canceled — user canceled3// android.test.refunded — refunded4// android.test.item_unavailable — product not found5 6val params = QueryProductDetailsParams.newBuilder()7 .setProductList(listOf(8 Product.newBuilder()9 .setProductId("android.test.purchased")10 .setProductType(ProductType.INAPP)11 .build()12 ))13 .build()ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Play Billing v7 production'a çıkarken: 1. ✅ Play Console'da internal test track'te dene (7 gün min) 2. ✅ Service account + Play Developer API access 3. ✅ RTDN endpoint live + TLS 4. ✅ Server-side validation 100% transactions için 5. ✅ Idempotent webhook handler (purchaseToken unique) 6. ✅ Refund handler → immediate entitlement revoke 7. ✅ Localized prices + descriptions (all supported locales) 8. ✅ "Manage subscription" deep link (Play Store intent) 9. ✅ Privacy policy + Terms of Service links 10. ✅ Grace period handling (account hold, pause) 11. ✅ A/B test pricing (Play Console native support) 12. ✅ Monitoring: conversion, churn, renewal, refund metrics **External Resources:** - [Play Billing Library v7 docs](https://developer.android.com/google/play/billing/integrate) - [RTDN documentation](https://developer.android.com/google/play/billing/rtdn-reference) - [Play Developer API](https://developers.google.com/android-publisher) - [Subscription plans guide](https://support.google.com/googleplay/android-developer/answer/12124625) - [Compose Material 3 pricing UI](https://m3.material.io/)
Sonuç
Play Billing v7 modern Android IAP'ın production standard'ı. Kotlin DSL + Coroutines + ProductDetails API ile temiz kod, multiple base plans + installment + winback offers ile flexibility, RTDN via Pub/Sub ile reliable state sync. Migration v6'dan 2-3 haftalık iş. Server-side validation olmadan go-live yapma — jailbreak/fraud riski yüksek. Compose Material 3 ile paywall UI güzel ve hızlı. iOS StoreKit 2 ile parity var, bazı özelliklerde (installment, multi-plan) aslında daha ileri.
*İlgili yazılar: StoreKit 2 Production, RN vs Flutter, Flutter Riverpod.*

