Tüm Yazılar
KategoriAndroid
Okuma Süresi
20 dk okuma
Yayın Tarihi
...
Kelime Sayısı
1.414kelime

Kahveni hazırla - bu içerikli bir makale!

Play Billing v7 yenilikler: Kotlin DSL setup, Product details API, installment plans, RTDN webhooks ve server-side receipt validation tam rehberi.

Google Play Billing Library v7: Android Subscription 2026

# 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

Ö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 handler
6 }
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 products
25 }
26 }
27 
28 override fun onBillingServiceDisconnected() {
29 // Retry logic with exponential backoff
30 }
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 token
12 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? = null
5) {
6 val productDetailsParams = BillingFlowParams.ProductDetailsParams.newBuilder()
7 .setProductDetails(productDetails)
8 
9 // Subscription için offerToken gerekli
10 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 error
19 }
20}
21 
22private fun handlePurchase(purchase: Purchase) = lifecycleScope.launch {
23 if (purchase.purchaseState != Purchase.PurchaseState.PURCHASED) return@launch
24 
25 // 1. Server-side validation
26 val valid = validateOnServer(purchase)
27 if (!valid) return@launch
28 
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 entitlement
45 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") // installment

Winback 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öster
4 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ştur
2gcloud pubsub topics create play-rtdn
3 
4# Service account ile subscribe
5gcloud pubsub subscriptions create my-backend-sub \
6 --topic play-rtdn \
7 --push-endpoint https://myapp.com/api/play-rtdn

Play 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.notificationType
6 switch (data.subscriptionNotification.notificationType) {
7 case 1: // SUBSCRIPTION_RECOVERED
8 case 2: // SUBSCRIPTION_RENEWED
9 await grantPro(data.subscriptionNotification.purchaseToken);
10 break;
11 case 3: // SUBSCRIPTION_CANCELED
12 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_EXPIRED
18 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: string
7): 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: purchaseToken
21 });
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?.autoRenewEnabled
27 };
28}

Compose UI Paywall

kotlin
1@Composable
2fun PaywallScreen(
3 vm: BillingViewModel = hiltViewModel(),
4 onDismiss: () -> Unit
5) {
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 = Modifier
12 .fillMaxSize()
13 .padding(16.dp),
14 verticalArrangement = Arrangement.spacedBy(12.dp)
15 ) {
16 Text(
17 "Pro'ya Yükselt",
18 style = MaterialTheme.typography.headlineLarge
19 )
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 Activity
30 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 purchase
2// android.test.canceled — user canceled
3// android.test.refunded — refunded
4// android.test.item_unavailable — product not found
5 
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.*

Etiketler

#Android#Kotlin#Play Billing#v7#IAP#Subscription#Google Play
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz