PAYATHON 2026

Unity IAP Subscription 购买时提示 Product not available

技术老齐

结论

Unity IAP 在 controller.products.all 中列出 Subscription,不等于 Google Play 已允许当前账号购买。这个集合也包含应用本地注册的商品定义。商品能否购买,要看 product.availableToPurchase、商店返回的元数据,以及 Play Console 中订阅和测试版本的状态。

这类问题通常与 Receipt Obfuscator、Public API Key 或购买代码无关,更常见的原因是以下配置尚未完成:

  • Subscription 尚未激活。
  • Subscription 没有可购买的 Base plan,或 Base plan 尚未激活。
  • 当前安装包不是从 Google Play 测试轨道安装的。
  • 测试账号不在测试名单中,或尚未通过测试链接加入对应轨道。
  • 安装包的 package name、签名或 version code 与 Play Console 中的应用不匹配。
  • IAP Catalog 中的 Google Play Store ID 配置错误。
  • 发布内容或商品配置尚未同步到 Google Play Billing。

为什么初始化成功后仍然无法购买

下面的代码只是把 IAP Catalog 中的商品注册到 Unity IAP:

builder.AddProduct(product.id, product.type, ids);

所以,即使 Google Play 没有返回有效的订阅详情,商品定义仍可能出现在:

controller.products.all

排查时要区分以下三个字段:

product.definition.id
product.definition.storeSpecificId
product.availableToPurchase
  • definition.id:Unity 项目内部使用的商品 ID。
  • definition.storeSpecificId:发送给当前商店的实际商品 ID。
  • availableToPurchase:Unity IAP 根据商店查询结果判断出的当前购买状态。

如果 availableToPurchase == false,调用:

m_Controller.InitiatePurchase(product);

可能会直接返回 ProductUnavailable,甚至不会打开 Google Play 购买界面。

Public API Key 和 Receipt Obfuscator 主要用于购买完成后的收据验证。如果购买流程尚未启动就出现 Product not available,通常不是它们导致的。

先确认运行时状态

不要只打印 Unity 内部 ID。建议把订阅的实际状态完整记录下来:

Product product = m_Controller.products.WithID(productID);

if (product == null)
{
    Debug.LogError($"Product not registered: {productID}");
    return;
}

Debug.Log(
    $"Unity ID: {product.definition.id}\n" +
    $"Store ID: {product.definition.storeSpecificId}\n" +
    $"Type: {product.definition.type}\n" +
    $"Enabled: {product.definition.enabled}\n" +
    $"Available: {product.availableToPurchase}\n" +
    $"Title: {product.metadata.localizedTitle}\n" +
    $"Price: {product.metadata.localizedPriceString}"
);

重点检查:

  1. Type 是否为 Subscription。
  2. Store ID 是否与 Play Console 中的 Subscription product ID 完全一致。
  3. Available 是否为 true。
  4. Title 和 Price 是否是 Google Play 返回的真实内容,而不是空值。

商品 ID 区分字符,不能带有多余空格。Unity 内部 ID 与 Google Play Store ID 也可以不同:

builder.AddProduct(
    "weekly_subscription",
    ProductType.Subscription,
    new IDs
    {
        { "com.test.google.weekly", GooglePlay.Name }
    }
);

这种情况下,应使用 Unity ID 查找商品:

m_Controller.products.WithID("weekly_subscription");

实际发送给 Google Play 的 ID 则是:

com.test.google.weekly

如果 IAP Catalog 中的通用 ID 与 Google Play ID 相同,两者都可以使用 com.test.google.weekly。

Play Console 中需要检查的配置

1. Subscription 和 Base plan 都必须可用

新版 Play Console 的订阅结构通常如下:

Subscription
└── Base plan
    └── Offer(可选)

仅创建 Subscription product ID 还不够,至少还要:

  • 创建 Base plan。
  • 设置计费周期和价格。
  • 配置销售国家或地区。
  • 激活 Base plan。
  • 确认订阅本身没有处于草稿状态,也没有待处理的配置。

Offer 通常是可选的,Base plan 才是完成实际购买所必需的配置。如果项目使用较旧的 Unity IAP 或 Google Billing 集成,还要确认该版本是否支持当前的 Base plan 和 Offer 模型。商品配置无误却仍然无法获取有效详情时,应优先升级到与当前 Google Play Billing 要求兼容的 Unity IAP 版本。

2. 应用必须上传到 Google Play

测试 Google Play Billing 的真实商品,不能只在设备上直接安装本地开发包。至少需要完成以下步骤:

  1. 使用与 Play Console 应用相同的 package name 构建 AAB。
  2. 将 AAB 上传到 Internal testing、Closed testing 或其他测试轨道。
  3. 发布测试版本。
  4. 将测试账号添加到对应轨道。
  5. 使用该账号打开测试加入链接并加入测试。
  6. 从 Google Play 商店页面安装应用。

如果先从 Google Play 安装应用,之后又使用 adb install 或 Unity Build & Run 覆盖安装,可能会造成签名、安装来源或版本不一致。排查时应先卸载应用,再完全通过测试轨道重新安装。

3. 核对 package name 和签名

运行时包名必须与 Play Console 中的应用完全一致:

Debug.Log($"Application identifier: {Application.identifier}");
Debug.Log($"Version: {Application.version}");

还需要检查:

  • applicationId 是否正确。
  • 上传包使用的签名配置是否正确。
  • 是否启用了 Play App Signing。
  • 本地安装的 APK 是否与测试轨道版本属于同一个应用。
  • versionCode 是否对应已经发布到测试轨道的版本。

Keystore 看起来正确,并不能证明设备上的安装包来自正确的测试版本。最可靠的验证方法仍然是从 Google Play 测试轨道安装应用。

4. 配置测试账号

测试账号通常要同时满足以下条件:

  • 已加入应用的 Internal testing 或 Closed testing 测试人员列表。
  • 已通过测试链接加入测试。
  • 必要时已加入 Play Console 的 License testing 列表。
  • Google Play 商店当前登录的是同一个账号。
  • 如果设备上有多个 Google 账号,要确认实际发起购买时使用的账号没有选错。

测试账号的身份需要明确。多账号设备很容易出现账号已经加入测试,但购买时仍提示商品不可用的情况。

5. 等待商店配置同步

发布 Subscription、Base plan、价格、测试轨道或测试人员配置后,这些变更不一定会立即在所有设备上生效。

确认配置已经发布后,可以:

  • 等待一段时间再测试。
  • 清除 Google Play Store 和 Google Play Services 的缓存。
  • 重启设备。
  • 卸载应用,再从测试轨道重新安装。

等待只能解决同步延迟。如果订阅仍处于 Draft、Inactive 状态,或者缺少 Base plan,等待不会使它变为可购买状态。

改进购买代码

购买前应直接拦截不可购买的商品,避免把 Unity IAP 返回的泛化错误误认为商店购买失败:

public void PurchaseButtonClick(string productID)
{
    if (m_PurchaseInProgress)
    {
        Debug.LogWarning("Purchase already in progress.");
        return;
    }

    if (m_Controller == null)
    {
        Debug.LogError("Unity IAP is not initialized.");
        return;
    }

    Product product = m_Controller.products.WithID(productID);

    if (product == null)
    {
        Debug.LogError($"Unknown product ID: {productID}");
        return;
    }

    Debug.Log(
        $"Buying product: {product.definition.id}, " +
        $"storeSpecificId: {product.definition.storeSpecificId}, " +
        $"type: {product.definition.type}, " +
        $"enabled: {product.definition.enabled}, " +
        $"availableToPurchase: {product.availableToPurchase}"
    );

    if (product.definition.type != ProductType.Subscription)
    {
        Debug.LogError(
            $"Product {product.definition.id} is not configured as a Subscription."
        );
        return;
    }

    if (!product.availableToPurchase)
    {
        Debug.LogError(
            $"Product {product.definition.id} is registered locally, " +
            $"but Google Play did not make store product " +
            $"{product.definition.storeSpecificId} available for purchase."
        );
        return;
    }

    m_PurchaseInProgress = true;
    m_Controller.InitiatePurchase(product);
}

同时保留详细的失败信息:

public void OnPurchaseFailed(Product product, PurchaseFailureReason reason)
{
    m_PurchaseInProgress = false;

    Debug.LogError(
        $"Purchase failed. " +
        $"Unity ID: {product.definition.id}, " +
        $"Store ID: {product.definition.storeSpecificId}, " +
        $"Reason: {reason}"
    );

    var description =
        m_TransactionHistoryExtensions?.GetLastPurchaseFailureDescription();

    if (description != null)
    {
        Debug.LogError(
            $"Store failure reason: {description.reason}, " +
            $"message: {description.message}"
        );
    }

    if (m_TransactionHistoryExtensions != null)
    {
        Debug.LogError(
            "Store-specific error code: " +
            m_TransactionHistoryExtensions
                .GetLastStoreSpecificPurchaseErrorCode()
        );
    }
}

不同 Unity IAP 版本提供的失败回调和字段可能略有不同,应以项目当前安装版本的 API 为准。

推荐排查顺序

按以下顺序检查通常更快:

  1. 打印 definition.id、storeSpecificId 和 availableToPurchase。
  2. 确认 IAP Catalog 中的类型是 ProductType.Subscription。
  3. 确认 Google Play Store ID 与 Subscription product ID 完全一致。
  4. 确认 Subscription 已激活。
  5. 确认至少有一个 Base plan 已激活,并配置了价格和销售地区。
  6. 确认 AAB 已发布到测试轨道。
  7. 确认测试账号已加入轨道,并通过测试链接接受测试。
  8. 卸载本地构建,再从 Google Play 测试轨道重新安装。
  9. 确认 Play Store 当前使用的是正确测试账号。
  10. 如果项目使用较旧的 Unity IAP,确认它支持当前的 Google Play Billing 和订阅模型。

判断问题的重点在 availableToPurchase。如果 controller.products.all 中有该商品,但 availableToPurchase == false,说明 Unity 已加载本地商品定义,却没有从 Google Play 获取到可购买的商店商品。此时应检查 Play Console 中 Subscription 和 Base plan 的激活状态,以及测试版本的发布与安装流程,不必继续调整 Receipt validator。

备注:内容仅供参考。