📖 PrzeglądOverview
Moduł Paywall umożliwia tworzenie ekranów zakupowych z automatycznym
pobieraniem cen z Google Play i obsługą różnych stylów wyświetlania.The Paywall module enables creating purchase screens with automatic price fetching from Google Play and support for various display styles.
InApp.The Paywall module requires an initialized InApp module.
Upewnij się, że wywołano ADict.init() przed użyciem Paywall.Make sure you called ADict.init() before using Paywall.
- Nawiązuje połączenie z Google Play BillingBilling
- Pobiera szczegóły produktów (nazwa, cena, opis)Fetches product details (name, price, description)
- Zarządza cyklem życia połączeniaManages connection lifecycle
// 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 -> { }
}
}
⚙️ WymaganiaRequirements
Przed użyciem modułu Paywall należy skonfigurować moduł InApp:Before using the Paywall module, configure the InApp module:
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łądError: $code - $msg")
}
})
}
}
startConnection()You don't need to manually call startConnection() -
Paywall automatycznie nawiązuje połączenie gdy jest wyświetlany i zarządza jego cyklem życia.Paywall automatically establishes connection when displayed and manages its lifecycle.
🎨 Style wyświetlaniaDisplay Styles
| Style | OpisDescription |
|---|---|
FULLSCREEN | Pełnoekranowy dialogFullscreen dialog |
BOTTOM_SHEET | Bottom sheet (domyślny)Bottom sheet (default) |
DIALOG | Mniejszy dialogSmaller dialog |
📝 Tworzenie paywall'aCreating a Paywall
displayNameIf you don't provide displayName,
displayPrice lub description, Paywall automatycznie pobierze je z Google PlayPaywall will automatically fetch them from Google Play
na podstawie productId.
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
}
}
▶️ PokazywanieDisplaying
show(id: String, activity: Activity, onResult?)
Pokaż paywall.Show paywall.
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 layoutFull custom layout
Jeśli chcesz użyć całkowicie własnego layoutu dla paywall'a:If you want to use a completely custom layout for paywall:
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 sekcjiCustom layouts for specific sections
Możesz też użyć customowych layoutów tylko dla wybranych sekcji (nagłówek, produkty, features, footer):You can also use custom layouts only for selected sections (header, products, features, footer):
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 sekcjiCustom layouts for specific sections
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ładExampleowy layout produktu 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 sukcesemPurchase successful
- Restored(productIds)Przywrócone zakupyPurchases restored
- DismissedZamknięty bez zakupuDismissed without purchase
- Error(message)BłądError
🔄 Cykl życiaLifecycle i połączenie z BillingBilling
Paywall automatycznie zarządza połączeniem z Google PlayPaywall automatically manages Google Play connection BillingBilling:
- Przy pokazaniu paywall'a: Nawiązuje połączenie z
BillingBillingClient - Po załadowaniu: Pobiera szczegóły produktów (ceny, nazwy, opisy)After loading: Fetches product details (prices, names, descriptions)
- Po zamknięciu paywall'a: Zwalnia referencję połączeniaAfter closing paywall: Releases connection reference
ADict.InApp.endConnection() np. w onDestroy() 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()
}
}
🔗 IntegracjaIntegration z InApp
Moduł Paywall jest ściśle zintegrowany z modułemThe Paywall module is tightly integrated with the InApp. Poniżej znajduje się kompletna konfiguracja wymagana do działania Paywall:Below is the complete configuration required for Paywall to work:
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łądError: $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
}
}
}
}
}
Paywall.show(), Paywall automatycznie:
- Sprawdza czy InApp jest zainicjalizowanyChecks if InApp is initialized
- Nawiązuje połączenie z BillingBillingClient (jeśli nie jest aktywne)
- Pobiera szczegóły produktów (nazwa, cena, opis)Fetches product details (name, price, description) z Google Play
- Sprawdza które produkty są już zakupioneChecks which products are already purchased
- Wyświetla paywall z aktualnymi cenamiDisplays paywall with current prices
Cykl życiaLifecycle zakupu
Gdy użytkownik kliknie przycisk zakupu w paywall:When user clicks the purchase button in paywall:
PaywallwywołujeInApp.launchSubscriptionPurchase()lubInApp.launchInappPurchase()- Google Play pokazuje dialog zakupuGoogle Play shows purchase dialog
- Po zakończeniu zakupu,
InAppautomatycznie wywołuje ACK/consumeAfter purchase completion,InAppautomatically calls ACK/consume InApp.Listener.onPurchaseAcknowledged/onPurchaseConsumedjest wywoływanyis calledPaywall.Result.Purchasedjest zwracany do callbacka
Paywall.Result.Purchased jest zwracany po
potwierdzeniu zakupu przez Google Play (ACK/consume), nie od razu po kliknięciu przycisku.purchase confirmation by Google Play (ACK/consume), not immediately after clicking the button.
❌ Obsługa błędówError Handling
| BłądError | OpisDescription | RozwiązanieSolution |
|---|---|---|
InApp module not initialized |
Moduł InApp nie został zainicjalizowanyInApp module was not initialized | Wywołaj ADict.init(context) w ApplicationCall ADict.init(context) in Application |
Paywall not found |
Paywall o podanym ID nie istniejePaywall with the given ID does not exist | Upewnij się, że wywołano ADict.Paywall.create("id")Make sure ADict.Paywall.create("id") was called |
Billing error |
BłądError połączenia z Google PlayGoogle Play connection error | Sprawdź logi, upewnij się że aplikacja jest opublikowana w Google Play ConsoleCheck logs, make sure the app is published in Google Play Console |