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}"
);
重点检查:
Type是否为Subscription。Store ID是否与 Play Console 中的 Subscription product ID 完全一致。Available是否为true。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 的真实商品,不能只在设备上直接安装本地开发包。至少需要完成以下步骤:
- 使用与 Play Console 应用相同的 package name 构建 AAB。
- 将 AAB 上传到 Internal testing、Closed testing 或其他测试轨道。
- 发布测试版本。
- 将测试账号添加到对应轨道。
- 使用该账号打开测试加入链接并加入测试。
- 从 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 为准。
推荐排查顺序
按以下顺序检查通常更快:
- 打印
definition.id、storeSpecificId和availableToPurchase。 - 确认 IAP Catalog 中的类型是
ProductType.Subscription。 - 确认 Google Play Store ID 与 Subscription product ID 完全一致。
- 确认 Subscription 已激活。
- 确认至少有一个 Base plan 已激活,并配置了价格和销售地区。
- 确认 AAB 已发布到测试轨道。
- 确认测试账号已加入轨道,并通过测试链接接受测试。
- 卸载本地构建,再从 Google Play 测试轨道重新安装。
- 确认 Play Store 当前使用的是正确测试账号。
- 如果项目使用较旧的 Unity IAP,确认它支持当前的 Google Play Billing 和订阅模型。
判断问题的重点在 availableToPurchase。如果 controller.products.all 中有该商品,但 availableToPurchase == false,说明 Unity 已加载本地商品定义,却没有从 Google Play 获取到可购买的商店商品。此时应检查 Play Console 中 Subscription 和 Base plan 的激活状态,以及测试版本的发布与安装流程,不必继续调整 Receipt validator。
备注:内容仅供参考。
