📖 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

StyleOpis
FULLSCREENPełnoekranowy dialog
BOTTOM_SHEETBottom sheet (domyślny)
DIALOGMniejszy 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:

⚠️ 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:
  1. Sprawdza czy InApp jest zainicjalizowany
  2. Nawiązuje połączenie z BillingClient (jeśli nie jest aktywne)
  3. Pobiera szczegóły produktów (nazwa, cena, opis) z Google Play
  4. Sprawdza które produkty są już zakupione
  5. Wyświetla paywall z aktualnymi cenami

Cykl życia zakupu

Gdy użytkownik kliknie przycisk zakupu w paywall:

  1. Paywall wywołuje InApp.launchSubscriptionPurchase() lub InApp.launchInappPurchase()
  2. Google Play pokazuje dialog zakupu
  3. Po zakończeniu zakupu, InApp automatycznie wywołuje ACK/consume
  4. InApp.Listener.onPurchaseAcknowledged/onPurchaseConsumed jest wywoływany
  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.

❌ Obsługa błędów

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