如何通过 Billing Library 判断用户是否符合 Free Trial 资格?
结论
Billing Library 不提供“用户是否使用过 Free Trial”的独立布尔值,也不能查询某个 Google Play 账号的试用历史。
较可靠的判断方式是调用 queryProductDetailsAsync(),查询当前 Google Play 账号可以购买的订阅方案,再检查返回的 subscriptionOfferDetails 中是否有包含免费试用阶段的 offer。Google Play 会根据订阅后台配置和当前账号的资格,筛选可用的 offer。
因此,UI 应表述为“当前可购买方案包含免费试用”,不能声称已经确认“用户从未使用过试用”。用户最终是否符合资格,仍以 Google Play 结算界面为准。
判断流程
- 在 Google Play Console 中为订阅配置免费试用 offer。
- 按业务需要设置资格条件,例如仅允许从未订阅过该应用订阅的用户参加。
- App 调用
queryProductDetailsAsync()获取当前可用的订阅方案。 - 在返回的
subscriptionOfferDetails中查找对应的offerId或offerTags。 - 检查该 offer 的
pricingPhases,确认其中存在价格为零的阶段。 - 如果找到,显示“可免费试用”,并在发起购买时使用该 offer 的
offerToken。 - 如果没有找到,展示正常订阅价格,不要直接断言用户“已经使用过试用”。
Kotlin 示例
以下示例假设免费试用 offer 配置了 free-trial 标签。用固定的 offerId 或 offerTags 识别试用方案,通常比只检查价格是否为零更稳妥。
private const val SUBSCRIPTION_ID = "premium_subscription"
private const val FREE_TRIAL_TAG = "free-trial"
data class TrialOffer(
val productDetails: ProductDetails,
val offerDetails: ProductDetails.SubscriptionOfferDetails
)
fun queryFreeTrialOffer(
billingClient: BillingClient,
onResult: (TrialOffer?) -> Unit
) {
val product = QueryProductDetailsParams.Product.newBuilder()
.setProductId(SUBSCRIPTION_ID)
.setProductType(BillingClient.ProductType.SUBS)
.build()
val params = QueryProductDetailsParams.newBuilder()
.setProductList(listOf(product))
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
onResult(null)
return@queryProductDetailsAsync
}
val productDetails = productDetailsList
.firstOrNull { it.productId == SUBSCRIPTION_ID }
val trialOffer = productDetails
?.subscriptionOfferDetails
.orEmpty()
.firstOrNull { offer ->
val isConfiguredTrialOffer =
FREE_TRIAL_TAG in offer.offerTags
val containsZeroPricePhase =
offer.pricingPhases.pricingPhaseList.any { phase ->
phase.priceAmountMicros == 0L
}
isConfiguredTrialOffer && containsZeroPricePhase
}
onResult(
if (productDetails != null && trialOffer != null) {
TrialOffer(productDetails, trialOffer)
} else {
null
}
)
}
}
UI 可以根据查询结果显示不同内容:
queryFreeTrialOffer(billingClient) { trialOffer ->
if (trialOffer != null) {
showSubscriptionMessage("可免费试用")
enableTrialPurchase(trialOffer)
} else {
showSubscriptionMessage("查看订阅价格")
showRegularSubscriptionOptions()
}
}
发起购买时,必须使用查询结果中的 offerToken:
fun launchTrialPurchase(
activity: Activity,
billingClient: BillingClient,
trialOffer: TrialOffer
): BillingResult {
val productDetailsParams =
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(trialOffer.productDetails)
.setOfferToken(trialOffer.offerDetails.offerToken)
.build()
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
.build()
return billingClient.launchBillingFlow(activity, billingFlowParams)
}
具体的回调类型和 ProductDetails 结果包装形式可能会随 Billing Library 版本变化,应以项目当前依赖版本的 API 签名为准。判断原则不受影响。
为什么不能直接查询“是否用过试用”
Google Play 会结合购买账号、订阅配置和 offer 条件计算试用资格。应用通常无法获得以下信息:
- 当前 Google Play 账号完整的订阅试用历史;
- 明确表示“此账号曾经使用过该试用”的字段;
- 用户在设备上切换 Google Play 账号的可靠通知;
- 能够用来自行复制 Google Play 资格规则的稳定账号标识。
queryPurchasesAsync() 只能查询当前有效购买等购买状态,无法证明用户过去从未参加过试用。对于已经取消、到期或通过另一个应用账号完成的购买,也不能靠它完整判断历史情况。
旧接口中的 SkuDetails.freeTrialPeriod 也只能说明商品配置了试用期,不能证明当前用户符合试用资格。
不要只依赖服务器记录
服务器应保存购买凭据、订阅状态,以及它们与应用账号之间的绑定关系,用于授权和防止重复发放权益。但服务器不能代替 Google Play 判断试用资格,原因包括:
- 一个应用账号可能对应多个 Google Play 账号;
- 多个应用账号可能使用同一个 Google Play 账号;
- 用户可能在购买前切换 Google Play 账号;
- Google Play 的资格规则可能会使用服务器无法获得的购买历史。
如果 offer 配置为由开发者自行判断资格,就需要通过自己的服务器完成资格判断,同时处理应用账号切换、重复注册和权益滥用等风险。这与 Google Play 自动判断的新用户 offer 属于不同的机制。
UI 和购买流程的注意事项
查到免费试用 offer 后,不要长期缓存结果。账号、订阅配置和资格都可能变化,建议在用户进入订阅页或点击购买前重新查询。
即使 App 显示“可免费试用”,提交购买时仍应以 Google Play 结算界面显示的价格和条款为准。查询失败时,也不能默认用户符合资格。此时可以显示“正在获取订阅方案”,或者只提供通用订阅入口。
建议按以下方式展示:
- 查到符合预期的试用 offer:显示具体的试用信息;
- 只查到普通方案:显示普通订阅价格;
- 查询失败:显示加载或重试状态;
- 不显示“你已经使用过免费试用”。缺少试用 offer 也可能与地区、商品配置、账号状态或临时查询问题有关。
备注:内容仅供参考。
