📖 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.

⚠️ WymaganieRequirement: Moduł Paywall wymaga zainicjalizowanego modułu 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.
💡 Automatyczne zarządzanie połączeniemAutomatic connection management: Paywall automatycznie:
  • 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
Szybki przykładQuick Example
// 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:

KonfiguracjaConfiguration 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łądError: $code - $msg")
            }
        })
    }
}
💡 Uwaga: Nie musisz ręcznie wywoływać 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

StyleOpisDescription
FULLSCREENPełnoekranowy dialogFullscreen dialog
BOTTOM_SHEETBottom sheet (domyślny)Bottom sheet (default)
DIALOGMniejszy dialogSmaller dialog

📝 Tworzenie paywall'aCreating a Paywall

💡 Automatyczne pobieranie szczegółów: Jeśli nie podasz 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.
Kompletna konfiguracjaComplete configuration
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.

Obsługa wynikuHandling result
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:

Własny layoutCustom 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 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):

Custom layouty sekcjiCustom section layouts
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

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 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:

⚠️ Ważne⚠️ Important: Paywall nie zamyka połączenia zPaywall doesn't close the connection with BillingBilling - pozostawia je dla dalszegoit leaves it for further użycia przez aplikację. Jeśli chcesz zakończyć połączenie, wywołaj ręcznieuse by the application. If you want to end the connection, call manually ADict.InApp.endConnection() np. w onDestroy() Activity.
Zalecana konfiguracja z cyklem życia ActivityRecommended configuration with Activity lifecycle
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:

Kompletna konfiguracjaComplete configuration w ApplicationComplete configuration in 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łą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
                }
            }
        }
    }
}
💡 Automatyczne zarządzanie połączeniemAutomatic connection management: Gdy wywołujesz Paywall.show(), Paywall automatycznie:
  1. Sprawdza czy InApp jest zainicjalizowanyChecks if InApp is initialized
  2. Nawiązuje połączenie z BillingBillingClient (jeśli nie jest aktywne)
  3. Pobiera szczegóły produktów (nazwa, cena, opis)Fetches product details (name, price, description) z Google Play
  4. Sprawdza które produkty są już zakupioneChecks which products are already purchased
  5. 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:

  1. Paywall wywołuje InApp.launchSubscriptionPurchase() lub InApp.launchInappPurchase()
  2. Google Play pokazuje dialog zakupuGoogle Play shows purchase dialog
  3. Po zakończeniu zakupu, InApp automatycznie wywołuje ACK/consumeAfter purchase completion, InApp automatically calls ACK/consume
  4. InApp.Listener.onPurchaseAcknowledged/onPurchaseConsumed jest wywoływanyis called
  5. Paywall.Result.Purchased jest 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.purchase confirmation by Google Play (ACK/consume), not immediately after clicking the button.

Obsługa błędówError Handling

BłądErrorOpisDescriptionRozwią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