PAYATHON 2026

在 Android Studio 中管理 Google Play 订阅免费试用

AI 小顾

结论

这不是等待同步就能解决的问题。使用 Google Play Billing Library 4.0 时,应用通过 querySkuDetailsAsync() 和 SkuDetails 只能访问订阅中配置为“向后兼容”的基础方案和优惠,无法查询、选择或应用新的“Developer determined”订阅 offer。

因此:

  • freeTrialPeriod 一直为空,通常说明当前免费试用 offer 没有作为 Billing Library 4.0 可识别的向后兼容优惠返回,或者它本身不符合兼容条件。
  • Billing Library 4.0 没有 offerToken,launchBillingFlow() 也没有参数可用来决定“应用免费试用”还是“按原价购买”。
  • 应用无法根据用户身份,让同一个 SkuDetails 动态切换是否使用免费试用。
  • 要实现“客户 A 使用免费试用,客户 B 立即付费”,需要升级到 Billing Library 5.0 或更高版本,并通过 ProductDetails 选择不同的订阅 offer。
  • 如果暂时不能升级,只能使用向后兼容配置,或者分别建立“含试用”和“不含试用”两个订阅商品,再由应用选择对应的 SKU。这只是过渡方案,不等同于新版 offer 机制。

为什么 freeTrialPeriod 为空

Google Play 的新订阅模型将商品拆分为:

  • Subscription
  • Base plan
  • Offer
  • Pricing phase

Billing Library 5.0 引入了 ProductDetails、SubscriptionOfferDetails 和 offerToken。应用可以获取同一订阅下的多个基础方案及优惠,并在购买时明确选择其中一个。

Billing Library 4.0 使用旧接口:

querySkuDetailsAsync()
SkuDetails
BillingFlowParams.setSkuDetails()

这些接口无法识别新版 offer 的 offerToken,也就不能完整表示同一订阅下的多个基础方案和优惠。为了兼容旧版应用,Google Play 只能向旧接口提供指定为向后兼容的基础方案,以及符合兼容条件的优惠。

“Developer determined” offer 要求应用先按自己的业务规则选择 offer,再将对应的 offerToken 传给 Google Play。Billing Library 4.0 无法完成这一步,因此不能用它获取并选择这类 offer。

修改 Play Console 配置后,Google Play 的确可能需要一段时间完成传播。但无论等待多久,Billing Library 4.0 都不会获得它本身不支持的 offer 选择能力。即使之后 freeTrialPeriod 出现,也只能说明 Play 返回了一个旧版可见的兼容优惠,并不表示应用可以在购买时自由开启或关闭试用。

Billing Library 4.0 下的购买行为

当前代码:

val flowParams = BillingFlowParams.newBuilder()
    .setSkuDetails(skuDetails)
    .build()

billingClient.launchBillingFlow(activity, flowParams)

这段代码只会把一个完整的 SkuDetails 交给 Google Play。Billing Library 4.0 没有下面这样的接口:

setFreeTrialEnabled(true)

也没有可以单独指定免费试用期的 API call。

购买是否包含试用,已经由该 SkuDetails 对应的兼容定价配置决定。调用 launchBillingFlow() 后,应用无法覆盖这个条件。

Scenario 1:客户 A 应获得免费试用

如果 Billing Library 4.0 查询到的 SkuDetails 确实包含:

{
  "productId": "my_monthly_plan",
  "price": "€49.00",
  "freeTrialPeriod": "P1M"
}

直接使用该 SkuDetails 启动购买流程即可,不需要向 launchBillingFlow() 额外传递免费试用参数:

val flowParams = BillingFlowParams.newBuilder()
    .setSkuDetails(skuDetails)
    .build()

val result = billingClient.launchBillingFlow(activity, flowParams)

是否提供免费试用,以及首次扣款日期,最终应以 Google Play 购买确认界面显示的内容为准。购买界面通常会列出:

  • 当前是否免费;
  • 试用持续时间;
  • 首次扣款日期;
  • 试用结束后的续订价格。

不能只看 freeTrialPeriod 就认定用户一定不会立即扣款。受 Google Play 资格限制、历史购买记录、账号状态或商品配置影响,用户可能无法获得试用。应用可以在发起购买前显示预期价格,但最终交易条件仍以 Play 购买界面和购买后的订阅状态为准。

服务端还应验证购买,并读取订阅的实际到期时间、付款状态和续订状态,不能只根据客户端的 SkuDetails 判断试用是否生效。

Scenario 2:客户 B 不应获得免费试用

如果客户 B 使用的仍是同一个包含免费试用的 SkuDetails,Billing Library 4.0 无法通知 Google Play 忽略试用并立即收取 €49。

下面的调用无法区分客户 A 和客户 B:

BillingFlowParams.newBuilder()
    .setSkuDetails(skuDetails)
    .build()

只要传入相同的 SkuDetails,应用就无法另外指定一条不同的定价路径。

“Developer determined”并不是让应用在旧版购买接口中传入一个布尔值。它的含义是,开发者通过业务逻辑决定是否向用户提供某个 offer,再通过新版 API 选择对应的 offerToken。

无法升级时的可行方案

方案一:使用向后兼容优惠

如果免费试用面向所有符合 Play 资格的首次订阅用户,可以在 Play Console 中配置 Billing Library 4.0 能访问的向后兼容基础方案及优惠。

之后继续使用:

querySkuDetailsAsync()

并检查:

skuDetails.freeTrialPeriod

这种方式能让旧版客户端获得免费试用信息,但不能根据开发者自己的条件分别处理客户 A 和客户 B。哪些 offer 可以设为向后兼容,以及相应的控制台入口,可能会随 Play Console 界面调整,应以当前控制台显示的兼容性选项为准。

方案二:建立两个不同的订阅商品

如果必须继续使用 Billing Library 4.0,同时又要由应用决定是否提供试用,可以建立两个独立 SKU,例如:

  • my_monthly_plan_with_trial
  • my_monthly_plan_without_trial

第一个 SKU 配置旧版客户端能够获取的免费试用,第二个 SKU 从首次购买开始按正常价格收费。应用根据服务端返回的业务资格选择 SKU:

val selectedSku = if (customerCanUseTrial) {
    "my_monthly_plan_with_trial"
} else {
    "my_monthly_plan_without_trial"
}

val params = SkuDetailsParams.newBuilder()
    .setSkusList(listOf(selectedSku))
    .setType(BillingClient.SkuType.SUBS)
    .build()

billingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
    if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
        return@querySkuDetailsAsync
    }

    val skuDetails = skuDetailsList?.firstOrNull() ?: return@querySkuDetailsAsync

    val flowParams = BillingFlowParams.newBuilder()
        .setSkuDetails(skuDetails)
        .build()

    billingClient.launchBillingFlow(activity, flowParams)
}

业务资格最好由服务端计算,不要只在客户端保存一个 Boolean,否则容易被篡改。

这种方案的代价也很明确:

  • 两个 SKU 是两个不同的订阅商品;
  • 升降级、替换订阅和权益合并会更复杂;
  • 报表、收入统计及服务端商品映射都要同时处理两个 SKU;
  • 必须防止同一用户意外购买两个订阅;
  • 只在应用中隐藏试用 SKU,无法真正替代 Google Play 的资格控制。

因此,这种做法更适合作为无法升级期间的临时兼容方案。

升级后的正确实现方式

升级到支持新版订阅模型的 Billing Library 后,应使用 queryProductDetailsAsync() 获取 ProductDetails。然后在 subscriptionOfferDetails 中找到所需的基础方案或优惠,并将对应的 offerToken 放入购买参数。

示意代码如下:

val selectedOffer =
    if (customerCanUseTrial) {
        productDetails.subscriptionOfferDetails
            ?.firstOrNull { offer ->
                offer.offerTags.contains("free-trial")
            }
    } else {
        productDetails.subscriptionOfferDetails
            ?.firstOrNull { offer ->
                offer.offerId == null
            }
    }

val offerToken = selectedOffer?.offerToken ?: return

val productDetailsParams =
    BillingFlowParams.ProductDetailsParams.newBuilder()
        .setProductDetails(productDetails)
        .setOfferToken(offerToken)
        .build()

val flowParams = BillingFlowParams.newBuilder()
    .setProductDetailsParamsList(listOf(productDetailsParams))
    .build()

billingClient.launchBillingFlow(activity, flowParams)

识别免费试用 offer 时,应根据自己的 basePlanId、offerId 或 offerTags 进行判断,不要只依赖列表顺序。正常原价方案也必须从 Play 返回的可购买选项中选择,不能自行构造 offerToken。

还需要注意的事项

  • freeTrialPeriod 使用 ISO 8601 period 格式,P1M 表示一个月,不是固定的 30 天。
  • 不要把 price 或 freeTrialPeriod 当作最终扣款凭据。购买确认界面和服务端验证结果才是可靠的交易状态来源。
  • 测试时应使用 Play Console 配置的许可测试账号,并通过 Google Play 安装包名、签名和发布轨道均正确的应用。
  • 确认订阅、基础方案和优惠都已激活,并且适用于测试账号所在的国家或地区。
  • Developer-determined eligibility 只表示资格由开发者判断,不会让 Billing Library 4.0 自动获得按用户选择 offer 的能力。
  • 不要尝试修改 SkuDetails JSON、价格字段或 freeTrialPeriod 来改变购买条件。这些内容由 Google Play 提供,客户端无法自行创建有效优惠。

简而言之,Billing Library 4.0 只能使用有限的向后兼容订阅配置,不能选择 Developer-determined offer。如果必须为不同用户分别提供“试用”和“立即付费”,应升级并使用 offerToken。升级前只能通过两个独立 SKU 等方式暂时兼容。

备注:内容仅供参考。