Paywall
Kreator paywall'i z produktami i funkcjami
📖 Przegląd
Moduł Paywall umożliwia tworzenie ekranów zakupowych z automatycznym
pobieraniem cen z Google Play i obsługą różnych stylów wyświetlania.
⚠️ Wymaganie: Moduł Paywall wymaga zainicjalizowanego modułu
InApp.
Upewnij się, że wywołano ADict.init() przed użyciem Paywall.
💡 Automatyczne zarządzanie połączeniem: Paywall automatycznie:
- Nawiązuje połączenie z Google Play Billing
- Pobiera szczegóły produktów (nazwa, cena, opis)
- Zarządza cyklem życia połączenia
Szybki przykład
// 1. Konfiguracja InApp (raz, np. w Application.onCreate)
ADict.InApp.setKnownProducts(
subs = setOf("pro_monthly", "pro_yearly"),
inappNonConsumable = setOf("pro_lifetime")
)
// 2. Definiowanie paywall'a
ADict.Paywall.create("premium") {
style(Style.BOTTOM_SHEET)
header {
title = "Przejdź na Premium"
subtitle = "Odblokuj wszystkie funkcje"
}
features {
feature("Bez reklam", R.drawable.ic_no_ads)
feature("Tryb offline", R.drawable.ic_offline)
}
products {
product("pro_monthly") { highlighted = true }
product("pro_yearly") { badge = "Oszczędź 40%" }
}
}
// 3. Pokazanie (automatyczne połączenie z Billing)
ADict.Paywall.show("premium", activity) { result ->
when (result) {
is Paywall.Result.Purchased -> enablePremium()
is Paywall.Result.Dismissed -> { }
}
}
⚙️ Wymagania
Przed użyciem modułu Paywall należy skonfigurować moduł InApp:
Konfiguracja InApp dla Paywall
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 1. Inicjalizacja ADict (wymaga kontekstu)
ADict.init(this)
// 2. Zdefiniuj znane produkty
ADict.InApp.setKnownProducts(
subs = setOf("pro_monthly", "pro_yearly"),
inappConsumable = setOf("coins_100", "coins_500"),
inappNonConsumable = setOf("pro_lifetime", "remove_ads")
)
// 3. Opcjonalnie: listener dla zdarzeń zakupowych
ADict.InApp.setListener(object : InApp.Listener {
override fun onBillingReady() {
Log.d("Billing", "Gotowy do płatności")
}
override fun onBillingDisconnected() {
Log.d("Billing", "Rozłączono")
}
override fun onPurchaseAcknowledged(productId: String) {
Log.d("Billing", "Zakup potwierdzony: $productId")
// Włącz premium, usunięcie reklam, etc.
}
override fun onPurchaseConsumed(productId: String, token: String) {
Log.d("Billing", "Zakup skonsumowany: $productId")
// Dodaj monety, etc.
}
override fun onBillingError(msg: String?, code: Int) {
Log.e("Billing", "Błąd: $code - $msg")
}
})
}
}
💡 Uwaga: Nie musisz ręcznie wywoływać
startConnection() -
Paywall automatycznie nawiązuje połączenie gdy jest wyświetlany i zarządza jego cyklem życia.
🎨 Style wyświetlania
| Style | Opis |
|---|---|
FULLSCREEN | Pełnoekranowy dialog |
BOTTOM_SHEET | Bottom sheet (domyślny) |
DIALOG | Mniejszy dialog |
📝 Tworzenie paywall'a
💡 Automatyczne pobieranie szczegółów: Jeśli nie podasz
displayName,
displayPrice lub description, Paywall automatycznie pobierze je z Google Play
na podstawie productId.
Kompletna konfiguracja
ADict.Paywall.create("premium_paywall") {
// Styl
style(Style.BOTTOM_SHEET)
// Header
header {
title = "Przejdź na Premium"
subtitle = "Odblokuj pełny potencjał aplikacji"
imageRes = R.drawable.premium_header
// lub animationRes = R.raw.premium_animation (Lottie)
}
// Lista funkcji
features {
feature("✓ Bez reklam", R.drawable.ic_no_ads)
feature("✓ Nielimitowane pobieranie", R.drawable.ic_download)
feature("✓ Tryb offline", R.drawable.ic_offline)
feature("✓ Priorytetowe wsparcie", R.drawable.ic_support)
feature("✓ Ekskluzywne treści", R.drawable.ic_star)
}
// Produkty - szczegóły pobierane automatycznie z Google Play
products {
product("pro_monthly") {
highlighted = true
// displayName, displayPrice i description zostaną pobrane z Google Play
}
product("pro_yearly") {
badge = "Najlepsza wartość"
// Opcjonalnie: nadpisz pobrane wartości
description = "Oszczędź 40%"
}
product("pro_lifetime") {
productType = BillingClient.ProductType.INAPP
badge = "Jednorazowo"
}
}
// Footer
footer {
showRestoreButton = true
restoreButtonText = "Przywróć zakupy"
termsUrl = "https://myapp.com/terms"
privacyUrl = "https://myapp.com/privacy"
}
// Kolory i wygląd (opcjonalne)
colors {
backgroundColor = Color.WHITE
primaryColor = Color.parseColor("#6200EE")
textColor = Color.BLACK
secondaryTextColor = Color.GRAY
highlightColor = Color.parseColor("#FFD700")
// Kolor tła kart produktów (niewyróznionych)
cardBackgroundColor = Color.parseColor("#F5F5F5")
// Kolory badge
badgeColor = Color.parseColor("#FF5722") // Kolor tła badge
badgeTextColor = Color.WHITE // Kolor tekstu badge
ownedBadgeColor = Color.parseColor("#4CAF50") // Kolor badge "Posiadane"
ownedBadgeTextColor = Color.WHITE // Kolor tekstu badge "Posiadane"
// Tekst dla zakupionych produktów
ownedBadgeText = "✓ Posiadane"
// Zaokrąglone górne rogi dla BottomSheet (w dp)
cornerRadiusDp = 24f
}
}
▶️ Pokazywanie
show(id: String, activity: Activity, onResult?)
Pokaż paywall.
Obsługa wyniku
ADict.Paywall.show("premium", activity) { result ->
when (result) {
is Paywall.Result.Purchased -> {
val productId = result.productId
ADict.Analytics.logPurchase(productId, price, "USD")
enablePremiumFeatures()
showThankYouDialog()
}
is Paywall.Result.Restored -> {
val products = result.productIds
enablePremiumFeatures()
showRestoredDialog(products)
}
is Paywall.Result.Dismissed -> {
ADict.Analytics.log("paywall_dismissed")
}
is Paywall.Result.Error -> {
showErrorDialog(result.message)
}
}
}
🎨 Custom layout
Pełny custom layout
Jeśli chcesz użyć całkowicie własnego layoutu dla paywall'a:
Własny layout
ADict.Paywall.create("custom_premium") {
customLayout(R.layout.my_custom_paywall) { view, config, onPurchase ->
// Binduj widoki
view.findViewById<TextView>(R.id.title).text = config.header.title
// Produkty
config.products.forEach { product ->
// Tworzenie przycisków produktów
}
// Obsługa zakupu
view.findViewById<Button>(R.id.buyButton).setOnClickListener {
onPurchase("pro_monthly")
}
}
}
Custom layouty dla poszczególnych sekcji
Możesz też użyć customowych layoutów tylko dla wybranych sekcji (nagłówek, produkty, features, footer):
Custom layouty sekcji
ADict.Paywall.create("premium") {
header {
title = "Go Premium!"
subtitle = "Unlock all features"
}
products {
product("pro_monthly") { highlighted = true }
product("pro_yearly") { badge = "Save 40%" }
}
// Custom layouty dla poszczególnych sekcji
customLayouts {
// Customowy nagłówek
header(R.layout.paywall_header) { view, config ->
view.findViewById<TextView>(R.id.title).text = config.title
view.findViewById<TextView>(R.id.subtitle).text = config.subtitle
config.imageRes?.let {
view.findViewById<ImageView>(R.id.image).setImageResource(it)
}
}
// Customowa feature
feature(R.layout.paywall_feature) { view, config ->
view.findViewById<TextView>(R.id.text).text = config.text
config.iconRes?.let {
view.findViewById<ImageView>(R.id.icon).setImageResource(it)
}
}
// Customowy produkt
product(R.layout.paywall_product) { view, config, isOwned, onClick ->
view.findViewById<TextView>(R.id.name).text = config.displayName ?: config.productId
view.findViewById<TextView>(R.id.price).text = config.displayPrice ?: "..."
view.findViewById<TextView>(R.id.description).text = config.description
// Ukryj cenę dla zakupionych
if (isOwned) {
view.findViewById<TextView>(R.id.price).text = "✓ Owned"
view.alpha = 0.7f
view.isEnabled = false
} else {
view.setOnClickListener { onClick() }
}
// Badge
config.badge?.let {
view.findViewById<TextView>(R.id.badge).apply {
text = it
visibility = View.VISIBLE
}
}
// Highlighted
if (config.highlighted) {
view.setBackgroundResource(R.drawable.bg_product_highlighted)
}
}
// Customowy footer
footer(R.layout.paywall_footer) { view, config, onRestoreClick ->
view.findViewById<Button>(R.id.restoreBtn).apply {
text = config.restoreButtonText
setOnClickListener { onRestoreClick() }
}
}
}
}
Przykładowy layout produktu XML
res/layout/paywall_product.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:textColor="@color/white"
android:background="@drawable/bg_badge"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?colorPrimary" />
</LinearLayout>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
📦 Paywall.Result
sealed class Result
- Purchased(productId)Zakup zakończony sukcesem
- Restored(productIds)Przywrócone zakupy
- DismissedZamknięty bez zakupu
- Error(message)Błąd
🔄 Cykl życia i połączenie z Billing
Paywall automatycznie zarządza połączeniem z Google Play Billing:
- Przy pokazaniu paywall'a: Nawiązuje połączenie z
BillingClient - Po załadowaniu: Pobiera szczegóły produktów (ceny, nazwy, opisy)
- Po zamknięciu paywall'a: Zwalnia referencję połączenia
⚠️ Ważne: Paywall nie zamyka połączenia z Billing - pozostawia je dla dalszego
użycia przez aplikację. Jeśli chcesz zakończyć połączenie, wywołaj ręcznie
ADict.InApp.endConnection() np. w onDestroy() Activity.
Zalecana konfiguracja z cyklem życia Activity
class PurchaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Listener jest opcjonalny - Paywall działa bez niego
ADict.InApp.setListener(object : InApp.Listener {
override fun onPurchaseAcknowledged(productId: String) {
// Obsłuż sukces zakupu
enablePremiumFeatures(productId)
}
// ... pozostałe metody
})
}
private fun showPaywall() {
// Paywall automatycznie zarządza połączeniem
ADict.Paywall.show("premium", this) { result ->
when (result) {
is Paywall.Result.Purchased -> {
// Zakup rozpoczęty - rzeczywiste potwierdzenie
// przyjdzie przez InApp.Listener.onPurchaseAcknowledged
}
is Paywall.Result.Error -> {
showError(result.message)
}
else -> { }
}
}
}
override fun onDestroy() {
// Opcjonalnie: zakończ połączenie gdy Activity jest niszczone
ADict.InApp.endConnection()
super.onDestroy()
}
}
🔗 Integracja z InApp
Moduł Paywall jest ściśle zintegrowany z modułem InApp. Poniżej znajduje się kompletna konfiguracja wymagana do działania Paywall:
Kompletna konfiguracja w Application
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 1. Inicjalizacja ADict
ADict.init(this, debuggable = BuildConfig.DEBUG)
// 2. WYMAGANE: Zdefiniuj znane produkty dla InApp
ADict.InApp.setKnownProducts(
subs = setOf("pro_monthly", "pro_yearly"),
inappConsumable = setOf("coins_100", "gems_50"),
inappNonConsumable = setOf("pro_lifetime", "remove_ads")
)
// 3. Opcjonalnie: listener dla zdarzeń zakupowych
ADict.InApp.setListener(object : InApp.Listener {
override fun onBillingReady() {
Log.d("Billing", "Gotowy do płatności")
}
override fun onBillingDisconnected() {
Log.d("Billing", "Rozłączono z Google Play")
}
override fun onPurchaseAcknowledged(productId: String) {
Log.d("Billing", "Zakup potwierdzony: $productId")
// Obsługa non-consumable i subskrypcji
when (productId) {
"pro_lifetime", "pro_monthly", "pro_yearly" -> enablePremium()
"remove_ads" -> disableAds()
}
}
override fun onPurchaseConsumed(productId: String, token: String) {
Log.d("Billing", "Zakup skonsumowany: $productId")
// Obsługa consumable
when (productId) {
"coins_100" -> addCoins(100)
"gems_50" -> addGems(50)
}
}
override fun onBillingError(msg: String?, code: Int) {
Log.e("Billing", "Błąd: $code - $msg")
}
})
// 4. Zdefiniuj paywall (nie wymaga połączenia z Billing)
ADict.Paywall.create("premium") {
header { title = "Przejdź na Premium" }
products {
product("pro_monthly") {
highlighted = true
productType = BillingClient.ProductType.SUBS
}
product("pro_yearly") {
badge = "Oszczędź 40%"
productType = BillingClient.ProductType.SUBS
}
product("pro_lifetime") {
badge = "Jednorazowo"
productType = BillingClient.ProductType.INAPP
}
}
}
}
}
💡 Automatyczne zarządzanie połączeniem: Gdy wywołujesz
Paywall.show(), Paywall automatycznie:
- Sprawdza czy InApp jest zainicjalizowany
- Nawiązuje połączenie z BillingClient (jeśli nie jest aktywne)
- Pobiera szczegóły produktów (nazwa, cena, opis) z Google Play
- Sprawdza które produkty są już zakupione
- Wyświetla paywall z aktualnymi cenami
Cykl życia zakupu
Gdy użytkownik kliknie przycisk zakupu w paywall:
PaywallwywołujeInApp.launchSubscriptionPurchase()lubInApp.launchInappPurchase()- Google Play pokazuje dialog zakupu
- Po zakończeniu zakupu,
InAppautomatycznie wywołuje ACK/consume InApp.Listener.onPurchaseAcknowledged/onPurchaseConsumedjest wywoływanyPaywall.Result.Purchasedjest zwracany do callbacka
⚠️ Ważne:
Paywall.Result.Purchased jest zwracany po
potwierdzeniu zakupu przez Google Play (ACK/consume), nie od razu po kliknięciu przycisku.
❌ Obsługa błędów
| Błąd | Opis | Rozwiązanie |
|---|---|---|
InApp module not initialized |
Moduł InApp nie został zainicjalizowany | Wywołaj ADict.init(context) w Application |
Paywall not found |
Paywall o podanym ID nie istnieje | Upewnij się, że wywołano ADict.Paywall.create("id") |
Billing error |
Błąd połączenia z Google Play | Sprawdź logi, upewnij się że aplikacja jest opublikowana w Google Play Console |