📘 Paywall SDK – 使用文档从 A 到 Z(附带示例 JSON)
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
}
]
}
字段定义
付费墙级字段
| Field | Meaning |
|---|---|
| uiType | UI 映射(例如 paywall_full_01, paywall_dialog_01) |
| ctaId | 未选择套餐时的默认 CTA |
| enable | 打开/关闭付费墙 |
| timesOpen | 显示前的应用启动次数(0 = 始终显示) |
| delayButtonX | 关闭按钮出现前的延迟(秒) |
| timesFree | 以逗号分隔的免费启动次数列表 |
| showAdsClose | 允许通过广告关闭付费墙 |
套餐级字段
| Field | Meaning |
|---|---|
| packageId | 已售/激活的 SKU |
| packageIdOrigin | 原始 SKU(用于计算折扣) |
| packageType | weekly / 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 处理):
- 计费请求
- 验证
- 发出
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 实时更新 – 无需重新加载屏幕。