DEV Community

ViO Tech
ViO Tech

Posted on

📘 Paywall SDK – Tài liệu sử dụng TỪ A Z (kèm JSON mẫu)

Tài liệu này hướng dẫn toàn bộ quy trình Paywall SDK, từ:

  • Cấu trúc JSON config
  • Parse & cung cấp dữ liệu
  • Hiển thị UI (Fragment / Dialog)
  • Purchase & Sale / Retention flow
  • Best practices tối ưu conversion

Phù hợp cho Android Kotlin – app nội bộ & production.


1️⃣ Tổng quan kiến trúc Paywall

Remote Config / API / Local JSON
        ↓
PaywallDataProvider
        ↓
PaywallRepository
        ↓
BasePaywallFragment / BasePaywallDialog
        ↓
Paywall UI (Full / One / Dialog)
Enter fullscreen mode Exit fullscreen mode

2️⃣ JSON Paywall Config – Chuẩn sử dụng

2.1 JSON mẫu (FULL SCREEN)

{
  "uiType": "paywall_full_01",
  "ctaId": 0,
  "enable": true,
  "timesOpen": 0,
  "delayButtonX": 2,
  "timesFree": "1,2,9,10,14",
  "showAdsClose": false,
  "packages": [
    {
      "packageId": "test.com.year.discount",
      "packageIdOrigin": "test.com.yearly",
      "packageType": "yearly",
      "ctaId": 1,
      "isPopular": false,
      "isSelected": false
    },
    {
      "packageId": "test.com.month.discount",
      "packageIdOrigin": "test.com.monthly",
      "packageType": "weekly",
      "ctaId": 3,
      "isPopular": false,
      "isSelected": false
    },
    {
      "packageId": "test.com.week.offer",
      "packageType": "weekly",
      "ctaId": 2,
      "isPopular": true,
      "isSelected": true
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

2.2 Giải thích từng field

🔹 Root level

Field Ý nghĩa
uiType Mapping UI (paywall_full_01, paywall_dialog_01)
ctaId CTA mặc định nếu chưa chọn package
enable Bật / tắt paywall
timesOpen Số lần app mở mới show (0 = luôn check)
delayButtonX Delay hiển thị nút close (giây)
timesFree Danh sách lần sử dụng được free
showAdsClose Đóng paywall bằng ads

🔹 Package item

Field Ý nghĩa
packageId SKU sale / active
packageIdOrigin SKU gốc (để tính discount)
packageType weekly / monthly / yearly / lifetime
ctaId CTA riêng cho package
isPopular Hiển thị badge “Popular”
isSelected Auto select khi mở paywall

3️⃣ Parse JSON → PaywallData

fun String.parsePaywall(): PaywallData? {
    if (isBlank()) return null
    return Json {
        ignoreUnknownKeys = true
        explicitNulls = false
    }.decodeFromString(this)
}
Enter fullscreen mode Exit fullscreen mode

📌 JSON có thể lấy từ:

  • Firebase Remote Config
  • API backend
  • Local config

4️⃣ PaywallDataProvider – Cấp dữ liệu cho SDK

4.1 Cung cấp Paywall config

override fun getPaywallConfig(placementType: String): PaywallData? {
    return when (placementType) {
        PAYWALL_HOME_ICON -> homeIconJson.parsePaywall()
        PAYWALL_SETTING -> settingJson.parsePaywall()
        else -> null
    }
}
Enter fullscreen mode Exit fullscreen mode

4.2 Retention mapping

override fun getRetentionPlacement(current: String): String? {
    return when (current) {
        PAYWALL_HOME_ICON -> PAYWALL_HOME_ICON_SALE
        else -> null
    }
}
Enter fullscreen mode Exit fullscreen mode

5️⃣ PaywallRepository – Luồng sử dụng

5.1 Init

paywallRepository.initialize(isDebugMode = true)
Enter fullscreen mode Exit fullscreen mode

5.2 Check show paywall

if (paywallRepository.shouldShow(PAYWALL_HOME_ICON)) {
    showPaywall()
}
Enter fullscreen mode Exit fullscreen mode

5.3 Load data & product

val data = paywallRepository.getPaywallData(PAYWALL_HOME_ICON)
val packages = paywallRepository.getPackages(data)
Enter fullscreen mode Exit fullscreen mode

6️⃣ Hiển thị UI Paywall

6.1 Full Screen – Ví dụ PaywallFull01Fragment

class PaywallFull01Fragment :
    BasePaywallFragment<FragmentPaywallFull01Binding>(),
    PackageSubCallback {

    override val bindingInflater =
        FragmentPaywallFull01Binding::inflate

    private val adapter by lazy {
        PackageSubAdapter(this)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.rcvPackage.apply {
            layoutManager = GridLayoutManager(context, 3)
            adapter = this@PaywallFull01Fragment.adapter
        }

        binding.btnContinue.setOnClickListener {
            adapter.getPaywallProduct()?.let {
                buy(it.product)
            }
        }

        binding.imgClose.setOnClickListener { close() }
        binding.txtPolicy.setOnClickListener { openPolicy() }
        binding.txtTerm.setOnClickListener { openTerm() }
    }

    override fun bindData(
        config: PaywallData,
        packages: List<PaywallProduct>
    ) {
        adapter.submitData(packages)
    }

    override fun onSelectPackage(paywallProduct: PaywallProduct) {
        binding.btnContinue.setText(
            PaywallCtaUtils.getCtaStringResId(paywallProduct)
        )

        val descRes = PaywallDescUtils.getDescResIds(
            paywallProduct,
            PaywallDescUtils.LayoutType.FULL_SCREEN
        )

        if (descRes.descRes != 0) {
            binding.txtDescription.text = PaywallDescUtils.formatDescription(
                requireContext(),
                descRes.descRes,
                paywallProduct
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

📌 Khi dùng: onboarding, hard paywall, feature lock


6.2 Dialog Sale – Ví dụ PaywallDialog01

class PaywallDialog01 :
    BasePaywallDialog<DialogPaywall01Binding>() {

    override val bindingInflater =
        DialogPaywall01Binding::inflate

    private var currentItem: PaywallProduct? = null

    override fun bindData(
        config: PaywallData,
        packages: List<PaywallProduct>
    ) {
        val item = packages.firstOrNull() ?: return
        currentItem = item

        binding.txtPrice.text = item.product.priceText

        binding.btnContinue.setText(
            PaywallCtaUtils.getCtaStringResId(item)
        )

        val descRes = PaywallDescUtils.getDescResIds(
            item,
            PaywallDescUtils.LayoutType.DIALOG_1_ITEM
        )

        if (descRes.descRes != 0) {
            binding.txtDescription.text = PaywallDescUtils.formatDescription(
                requireContext(),
                descRes.descRes,
                item
            )
        }

        binding.btnContinue.setOnClickListener {
            currentItem?.let { buy(it) }
        }

        binding.imgClose.setOnClickListener { close() }
    }
}
Enter fullscreen mode Exit fullscreen mode

📌 Khi dùng: retention, sale off, exit intent


6.2 Dialog (Sale / Retention)

PaywallDialog01()
Enter fullscreen mode Exit fullscreen mode

Dùng cho:

  • Sale
  • Exit intent

7️⃣ Package selection & CTA

PaywallCtaUtils.getCtaStringResId(paywallProduct)
Enter fullscreen mode Exit fullscreen mode

CTA thay đổi theo:

  • Trial
  • Sale
  • Lifetime

8️⃣ Discount & Sale logic

  • packageIdOrigin ≠ null → enable sale
  • Tính %OFF từ giá gốc
  • Gạch ngang giá cũ

9️⃣ Purchase flow

buy(paywallProduct)
Enter fullscreen mode Exit fullscreen mode

SDK xử lý:

  • Billing
  • Verify
  • Emit purchaseStatus

🔟 Retention Flow (chuẩn)

Paywall A
  ↓ close
Sale Paywall Dialog
  ↓ close
No more paywall
Enter fullscreen mode Exit fullscreen mode

1️⃣1️⃣ Best Practices (Rất quan trọng)

✅ Full screen: 2–3 packages

✅ Dialog sale: 1 package

✅ Weekly = entry price thấp

✅ Yearly = best value

✅ Lifetime = anchor price


1️⃣2️⃣ Checklist tích hợp nhanh

  • [ ] JSON config hợp lệ
  • [ ] Map placement → config
  • [ ] Init repository
  • [ ] Test sandbox purchase
  • [ ] Test sale / retention

1️⃣3️⃣ Ví dụ thực tế: Tích hợp Paywall trong SettingFragment

Mục tiêu

  • Soft paywall (user chủ động mua)
  • Upsell nhẹ, không làm gián đoạn trải nghiệm
  • Cho phép quản lý subscription sau khi mua

Inject dependency

private val paywallRepository: PaywallRepository by inject()
private val launcher: PaywallLauncher by inject()
Enter fullscreen mode Exit fullscreen mode

Hiển thị Subscribe CTA theo config & trạng thái mua

ctlSub.isVisible =
    paywallRepository
        .getPaywallData(Constants.PaywallConfigKey.PAYWALL_SETTING)
        ?.enable == true
Enter fullscreen mode Exit fullscreen mode
txtSubDes.text = if (paywallRepository.isPurchased()) {
    getString(R.string.unlocked_all_features)
} else {
    getString(R.string.upgrade_to_unlock_all)
}
Enter fullscreen mode Exit fullscreen mode

Manage Subscription (chỉ khi là subscription)

ctlManageSubscription.isVisible =
    paywallRepository.isPurchased() &&
    paywallRepository.getCurrentPackagePurchased()?.type != PandaPackageType.LIFETIME
Enter fullscreen mode Exit fullscreen mode

📌 Lifetime không cần manage → UX chuẩn Play Store


Click Subscribe → Mở Paywall bằng Launcher

ctlSub.setOnClickListener {
    lifecycleScope.launch {
        val activity = activity ?: return@launch

        when (
            launcher.launch(
                activity,
                Constants.PaywallConfigKey.PAYWALL_SETTING
            )
        ) {
            PaywallResult.Success -> {
                ctlManageSubscription.isVisible =
                    paywallRepository.isPurchased() &&
                    paywallRepository.getCurrentPackagePurchased()?.type != PandaPackageType.LIFETIME

                txtSubDes.text = getString(R.string.unlocked_all_features)

                NativeAdManager.disableAllAds()
                InterManager.disableAllAds()
                RewardAdsManager.disableAllAds()
                BannerAdManager.disableAllAds()
                PandaResumeAd.INSTANCE?.disableAllAds()
            }

            PaywallResult.Dismissed -> {
                // User đóng paywall
            }

            PaywallResult.NotShown -> {
                // Đã mua hoặc chưa đủ điều kiện show
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Flow tổng quát

Setting Screen
   ↓
Subscribe CTA
   ↓ click
PaywallLauncher
   ↓
┌───────────────┬───────────────┬───────────────┐
│ Success       │ Dismissed     │ NotShown      │
│ Disable ads   │ Log event     │ Continue app  │
│ Update UI     │               │               │
└───────────────┴───────────────┴───────────────┘
Enter fullscreen mode Exit fullscreen mode

Best Practices cho Setting Paywall

✅ Soft paywall, không ép

✅ Không show paywall nếu enable = false

✅ Lifetime ≠ manage subscription

✅ Disable ads chỉ khi Success

✅ Update UI ngay, không reload màn hình

Top comments (0)