InApp Billing
Google Play Billing Library v8 - zakupy i subskrypcje
📖 Przegląd
Moduł InApp zapewnia prostą obsługę Google Play Billing Library v8.
Wspiera zakupy jednorazowe (consumable i non-consumable) oraz subskrypcje z automatycznym
acknowledge/consume.
setKnownProducts() - połączenie z Billing jest zarządzane automatycznie.
⚙️ Konfiguracja
setKnownProducts(subs, inappConsumable, inappNonConsumable)
Zdefiniuj znane produkty do automatycznego ACK/consume.
setListener(listener: Listener)
Ustaw listener dla zdarzeń billingowych.
// Zdefiniuj produkty
ADict.InApp.setKnownProducts(
subs = setOf("premium_monthly", "premium_yearly"),
inappConsumable = setOf("coins_100", "coins_500", "gems_pack"),
inappNonConsumable = setOf("remove_ads", "unlock_all")
)
// Listener zdarzeń
ADict.InApp.setListener(object : InApp.Listener {
override fun onBillingReady() {
Log.d("Billing", "Gotowy do płatności")
loadProducts()
}
override fun onBillingDisconnected() {
Log.d("Billing", "Rozłączono")
}
override fun onPurchaseAcknowledged(productId: String) {
Log.d("Billing", "Zakup potwierdzony: $productId")
when (productId) {
"premium_monthly", "premium_yearly" -> enablePremium()
"remove_ads" -> disableAds()
"unlock_all" -> unlockAllContent()
}
}
override fun onPurchaseConsumed(productId: String, token: String) {
Log.d("Billing", "Zakup skonsumowany: $productId")
when (productId) {
"coins_100" -> addCoins(100)
"coins_500" -> addCoins(500)
"gems_pack" -> addGems(50)
}
}
override fun onBillingError(msg: String?, code: Int) {
Log.e("Billing", "Błąd: $code - $msg")
showError("Błąd płatności")
}
})
🔌 Połączenie
startConnection()
Połącz z Google Play Billing.
endConnection()
Rozłącz.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Połącz przy starcie
ADict.InApp.startConnection()
}
override fun onDestroy() {
// Rozłącz przy zamknięciu
ADict.InApp.endConnection()
super.onDestroy()
}
}
📦 Produkty
queryProductDetails(productType, productIds, callback)
Pobierz szczegóły produktów z Google Play.
Parametry:
productType: String- BillingClient.ProductType.SUBS lub INAPPproductIds: List<String>- Lista ID produktówcallback: (Map<String, ProductDetails>) -> Unit
// Subskrypcje
ADict.InApp.queryProductDetails(
BillingClient.ProductType.SUBS,
listOf("premium_monthly", "premium_yearly")
) { products ->
products.forEach { (id, details) ->
val price = details.subscriptionOfferDetails
?.firstOrNull()
?.pricingPhases
?.pricingPhaseList
?.lastOrNull()
?.formattedPrice
Log.d("Product", "$id: ${details.name} - $price")
}
}
// Produkty jednorazowe
ADict.InApp.queryProductDetails(
BillingClient.ProductType.INAPP,
listOf("coins_100", "remove_ads")
) { products ->
products.forEach { (id, details) ->
val price = details.oneTimePurchaseOfferDetails?.formattedPrice
Log.d("Product", "$id: ${details.name} - $price")
}
}
🛒 Zakupy
launchInappPurchase(activity, productId)
Uruchom zakup produktu jednorazowego.
launchSubscriptionPurchase(activity, productId, offerToken?)
Uruchom zakup subskrypcji.
consume(purchase, callback)
Ręczne skonsumowanie zakupu.
// Zakup jednorazowy
fun buyCoins() {
ADict.InApp.launchInappPurchase(activity, "coins_100")
}
fun removeAds() {
ADict.InApp.launchInappPurchase(activity, "remove_ads")
}
// Zakup subskrypcji
fun buyPremiumMonthly() {
ADict.InApp.launchSubscriptionPurchase(activity, "premium_monthly")
}
// Subskrypcja z konkretną ofertą
fun buyPremiumYearlyWithTrial() {
ADict.InApp.fetchSubscriptionOfferToken(
productId = "premium_yearly",
preferTrialOrIntro = true
) { offerToken ->
ADict.InApp.launchSubscriptionPurchase(
activity,
"premium_yearly",
offerToken
)
}
}
📅 Subskrypcje
hasActiveSubscription(callback: (Boolean) -> Unit)
Sprawdź czy użytkownik ma aktywną subskrypcję.
hasActiveSubscriptionFor(productId, callback: (Boolean) -> Unit)
Sprawdź konkretną subskrypcję.
fetchSubscriptionOfferToken(productId, basePlanId, offerId, callback)
Pobierz offerToken dla konkretnego planu.
fetchSubscriptionOfferToken(productId, requiredOfferTags?, preferTrialOrIntro, callback)
Pobierz offerToken z preferencjami.
// Sprawdź status premium
fun checkPremiumStatus() {
ADict.InApp.hasActiveSubscription { hasSubscription ->
if (hasSubscription) {
enablePremiumFeatures()
} else {
showUpgradePrompt()
}
}
}
// Sprawdź konkretną subskrypcję
ADict.InApp.hasActiveSubscriptionFor("premium_yearly") { isYearly ->
if (isYearly) {
showYearlyBadge()
}
}
// Pobierz offerToken dla triala
ADict.InApp.fetchSubscriptionOfferToken(
productId = "premium_monthly",
preferTrialOrIntro = true
) { offerToken ->
if (offerToken != null) {
showTrialOffer()
}
}
// Pobierz offerToken po tagu
ADict.InApp.fetchSubscriptionOfferToken(
productId = "premium_yearly",
requiredOfferTags = setOf("special-offer"),
preferTrialOrIntro = false
) { offerToken ->
offerToken?.let {
launchSubscriptionWithOffer(it)
}
}
🔍 Zapytania
queryActivePurchases(productType, callback)
Pobierz aktywne zakupy użytkownika.
// Pobierz subskrypcje
ADict.InApp.queryActivePurchases(BillingClient.ProductType.SUBS) { purchases ->
purchases.forEach { purchase ->
Log.d("Purchase", "Subskrypcja: ${purchase.products}")
Log.d("Purchase", "Stan: ${purchase.purchaseState}")
Log.d("Purchase", "Token: ${purchase.purchaseToken}")
}
}
// Pobierz produkty jednorazowe
ADict.InApp.queryActivePurchases(BillingClient.ProductType.INAPP) { purchases ->
purchases.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED }
.forEach { purchase ->
purchase.products.forEach { productId ->
when (productId) {
"remove_ads" -> disableAds()
"unlock_all" -> unlockContent()
}
}
}
}
💡 Przykłady praktyczne
Kompletna implementacja
object BillingManager : InApp.Listener {
private val _isPremium = MutableStateFlow(false)
val isPremium: StateFlow<Boolean> = _isPremium
private val _coins = MutableStateFlow(0)
val coins: StateFlow<Int> = _coins
fun init() {
ADict.InApp.setKnownProducts(
subs = setOf("premium_monthly", "premium_yearly"),
inappConsumable = setOf("coins_100", "coins_500"),
inappNonConsumable = setOf("remove_ads")
)
ADict.InApp.setListener(this)
ADict.InApp.startConnection()
}
override fun onBillingReady() {
// Sprawdź status przy starcie
checkPurchaseStatus()
}
override fun onBillingDisconnected() {
// Auto-reconnect po 3 sekundach
Handler(Looper.getMainLooper()).postDelayed({
ADict.InApp.startConnection()
}, 3000)
}
override fun onPurchaseAcknowledged(productId: String) {
when (productId) {
"premium_monthly", "premium_yearly" -> {
_isPremium.value = true
ADict.EventBus.postSticky(EventBus.Events.PremiumStatusChanged(true))
ADict.Analytics.logSubscriptionStart(productId, 0.0, "USD")
}
"remove_ads" -> {
// Zapisz stan
ADict.SecureStorage.putBoolean("ads_removed", true)
}
}
}
override fun onPurchaseConsumed(productId: String, token: String) {
val coinsToAdd = when (productId) {
"coins_100" -> 100
"coins_500" -> 500
else -> 0
}
if (coinsToAdd > 0) {
_coins.value += coinsToAdd
ADict.SecureStorage.putInt("coins", _coins.value)
ADict.Analytics.logPurchase(productId, coinsToAdd / 100.0, "USD")
}
}
override fun onBillingError(msg: String?, code: Int) {
ADict.Analytics.logError("billing_error", msg, mapOf("code" to code))
}
private fun checkPurchaseStatus() {
// Sprawdź subskrypcje
ADict.InApp.hasActiveSubscription { hasActive ->
_isPremium.value = hasActive
}
// Sprawdź jednorazowe zakupy
ADict.InApp.queryActivePurchases(BillingClient.ProductType.INAPP) { purchases ->
purchases.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
purchase.products.forEach { productId ->
onPurchaseAcknowledged(productId)
}
}
}
}
// Załaduj zapisane coiny
_coins.value = ADict.SecureStorage.getInt("coins", 0)
}
// === Public API ===
fun buyPremium(activity: Activity, yearly: Boolean) {
val productId = if (yearly) "premium_yearly" else "premium_monthly"
ADict.InApp.launchSubscriptionPurchase(activity, productId)
}
fun buyCoins(activity: Activity, amount: Int) {
val productId = when (amount) {
100 -> "coins_100"
500 -> "coins_500"
else -> return
}
ADict.InApp.launchInappPurchase(activity, productId)
}
fun removeAds(activity: Activity) {
ADict.InApp.launchInappPurchase(activity, "remove_ads")
}
fun spendCoins(amount: Int): Boolean {
return if (_coins.value >= amount) {
_coins.value -= amount
ADict.SecureStorage.putInt("coins", _coins.value)
true
} else {
false
}
}
}
UI płatności
@Composable
fun PremiumScreen(
onClose: () -> Unit
) {
val isPremium by BillingManager.isPremium.collectAsState()
val context = LocalContext.current
val activity = context as Activity
var monthlyPrice by remember { mutableStateOf("") }
var yearlyPrice by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
ADict.InApp.queryProductDetails(
BillingClient.ProductType.SUBS,
listOf("premium_monthly", "premium_yearly")
) { products ->
products["premium_monthly"]?.let { details ->
monthlyPrice = details.subscriptionOfferDetails
?.firstOrNull()
?.pricingPhases
?.pricingPhaseList
?.lastOrNull()
?.formattedPrice ?: ""
}
products["premium_yearly"]?.let { details ->
yearlyPrice = details.subscriptionOfferDetails
?.firstOrNull()
?.pricingPhases
?.pricingPhaseList
?.lastOrNull()
?.formattedPrice ?: ""
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("🌟 Premium", style = MaterialTheme.typography.h4)
Spacer(modifier = Modifier.height(16.dp))
// Features
FeatureItem("✓ Bez reklam")
FeatureItem("✓ Wszystkie funkcje")
FeatureItem("✓ Priorytetowe wsparcie")
Spacer(modifier = Modifier.height(24.dp))
// Monthly
Button(
onClick = { BillingManager.buyPremium(activity, yearly = false) },
modifier = Modifier.fillMaxWidth()
) {
Text("Miesięczna - $monthlyPrice/mies")
}
Spacer(modifier = Modifier.height(8.dp))
// Yearly
Button(
onClick = { BillingManager.buyPremium(activity, yearly = true) },
modifier = Modifier.fillMaxWidth()
) {
Text("Roczna - $yearlyPrice/rok (oszczędź 40%)")
}
}
}
💰 Integracja z Paywall
Moduł Paywall automatycznie korzysta z InApp do obsługi płatności. Jeśli używasz Paywall, nie musisz ręcznie zarządzać połączeniem z Billing - Paywall robi to za Ciebie.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 1. Inicjalizacja ADict
ADict.init(this)
// 2. Konfiguracja produktów - wymagane dla Paywall
ADict.InApp.setKnownProducts(
subs = setOf("pro_monthly", "pro_yearly"),
inappNonConsumable = setOf("pro_lifetime", "remove_ads")
)
// 3. Listener (opcjonalny, ale zalecany)
ADict.InApp.setListener(object : InApp.Listener {
override fun onBillingReady() { /* gotowy */ }
override fun onBillingDisconnected() { /* rozłączony */ }
override fun onPurchaseAcknowledged(productId: String) {
// Zakup potwierdzony - włącz funkcje premium
enableFeature(productId)
}
override fun onPurchaseConsumed(productId: String, token: String) { }
override fun onBillingError(msg: String?, code: Int) { }
})
}
}
// W Activity - Paywall zarządza połączeniem automatycznie
ADict.Paywall.show("premium", activity) { result ->
when (result) {
is Paywall.Result.Purchased -> { /* sukces */ }
is Paywall.Result.Dismissed -> { /* zamknięty */ }
is Paywall.Result.Error -> { /* błąd */ }
else -> { }
}
}
startConnection()
ani endConnection() ręcznie - Paywall zarządza tym automatycznie.
📚 Listener Interface
interface InApp.Listener
- onBillingReady()Billing gotowy
- onBillingDisconnected()Rozłączono
- onPurchaseAcknowledged(productId)Zakup potwierdzony
- onPurchaseConsumed(productId, token)Zakup skonsumowany
- onBillingError(msg, code)Błąd