📘 Paywall SDK – 使用文档从 A 到 Z(附带示例 JSON)

发布: (2026年1月9日 GMT+8 12:24)
6 min read
原文: Dev.to

I’m happy to translate the article for you, but I need the full text that you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the text, I’ll translate it into Simplified Chinese while preserving all formatting, markdown, and technical terms as requested.

Paywall SDK – 快速参考

目标: Android Kotlin(内部与正式应用)
范围:

  • JSON 配置结构
  • 解析与数据提供
  • UI 渲染(Fragment / Dialog)
  • 购买、销售与留存流程
  • 转化率优化最佳实践

架构流程

Remote Config / API / Local JSON

   PaywallDataProvider

    PaywallRepository

BasePaywallFragment / BasePaywallDialog

   Paywall UI (Full / One / Dialog)

示例 JSON 配置

{
  "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
    }
  ]
}

字段定义

付费墙级字段

FieldMeaning
uiTypeUI 映射(例如 paywall_full_01, paywall_dialog_01
ctaId未选择套餐时的默认 CTA
enable打开/关闭付费墙
timesOpen显示前的应用启动次数(0 = 始终显示)
delayButtonX关闭按钮出现前的延迟(秒)
timesFree以逗号分隔的免费启动次数列表
showAdsClose允许通过广告关闭付费墙

套餐级字段

FieldMeaning
packageId已售/激活的 SKU
packageIdOrigin原始 SKU(用于计算折扣)
packageTypeweekly / monthly / yearly / lifetime
ctaId此套餐专属 CTA
isPopular显示 “Popular” 徽章
isSelected付费墙打开时自动选中

解析助手

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

JSON 的来源

  • Firebase 远程配置
  • 后端 API
  • 本地文件

仓库使用

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

override fun getRetentionPlacement(current: String): String? {
    return when (current) {
        PAYWALL_HOME_ICON -> PAYWALL_HOME_ICON_SALE
        else -> null
    }
}
paywallRepository.initialize(isDebugMode = true)

if (paywallRepository.shouldShow(PAYWALL_HOME_ICON)) {
    showPaywall()
}

val data = paywallRepository.getPaywallData(PAYWALL_HOME_ICON)
val packages = paywallRepository.getPackages(data)

全屏付费墙碎片(示例)

class PaywallFull01Fragment :
    BasePaywallFragment(),
    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) {
        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
            )
        }
    }
}

单包付费墙对话框(示例)

class PaywallDialog01 :
    BasePaywallDialog() {

    override val bindingInflater = DialogPaywall01Binding::inflate
    private var currentItem: PaywallProduct? = null

    override fun bindData(config: PaywallData, packages: List) {
        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() }
    }
}

使用场景 – 引导、硬付费墙、功能锁定、用户留存、促销、退出意图。

CTA 逻辑

PaywallCtaUtils.getCtaStringResId(paywallProduct)
  • CTA 根据 试用 / 销售 / 终身 变化。
  • 如果 packageIdOrigin != null → 启用销售模式。
  • 折扣百分比根据原价计算。
  • 原价使用删除线显示。

购买流程(由 SDK 处理):

  1. 计费请求
  2. 验证
  3. 发出 purchaseStatus
Paywall A  → close
Sale Paywall Dialog → close
→ No further paywall

快速检查清单

  • 有效的 JSON 配置
  • Placement → 配置映射
  • 仓库初始化

推荐的 UI 模式

UI 类型常见套餐备注
全屏2‑3 个套餐适合发现
对话框(促销)1 个套餐适用于退出意向或限时优惠
每周低入门价入门级诱因
每年最佳价值鼓励长期承诺
终身锚定价高价值追加销售

订阅流程概览

  • [ ] 测试沙盒购买
  • [ ] 测试销售/保留

软付费墙(用户主动购买)

  • 轻度追加销售 – 不会中断用户体验。
  • 购买后可管理订阅。

Kotlin 实现

private val paywallRepository: PaywallRepository by inject()
private val launcher: PaywallLauncher by inject()

// Show the Subscribe CTA only when the paywall is enabled
ctlSub.isVisible =
    paywallRepository
        .getPaywallData(Constants.PaywallConfigKey.PAYWALL_SETTING)
        ?.enable == true

// Text description depends on purchase state
txtSubDes.text = if (paywallRepository.isPurchased()) {
    getString(R.string.unlocked_all_features)
} else {
    getString(R.string.upgrade_to_unlock_all)
}

// Show “Manage subscription” only for non‑lifetime purchases
ctlManageSubscription.isVisible =
    paywallRepository.isPurchased() &&
    paywallRepository.getCurrentPackagePurchased()?.type != PandaPackageType.LIFETIME

// 📌 Lifetime subscriptions do **not** need a “manage” button → matches Play Store UX
ctlSub.setOnClickListener {
    lifecycleScope.launch {
        val activity = activity ?: return@launch

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

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

                // Disable all ad formats
                NativeAdManager.disableAllAds()
                InterManager.disableAllAds()
                RewardAdsManager.disableAllAds()
                BannerAdManager.disableAllAds()
                PandaResumeAd.INSTANCE?.disableAllAds()
            }

            PaywallResult.Dismissed -> {
                // User closed the paywall
            }

            PaywallResult.NotShown -> {
                // Already purchased or not eligible to show the paywall
            }
        }
    }
}

UI 流程图

Setting Screen

Subscribe CTA
   ↓ click
PaywallLauncher

┌───────────────┬───────────────┬───────────────┐
│   Success     │   Dismissed   │   NotShown    │
│ Disable ads   │ Log event     │ Continue app  │
│ Update UI     │               │               │
└───────────────┴───────────────┴───────────────┘

检查清单

  • ✅ 软付费墙 – 不强制购买。
  • ✅ 当 enable = false 时,付费墙会隐藏。
  • ✅ 终身订阅 “管理订阅”。
  • ✅ 广告仅在 Success 时被禁用
  • ✅ UI 实时更新 – 无需重新加载屏幕。
Back to Blog

相关文章

阅读更多 »

新年快乐,社区!

引言 大家好,感谢阅读本博客,祝大家新年快乐! 项目概述 本项目的最终目标是一个纯粹的...