Blazor WASM PWA 中 Google Play Billing 无法获取订阅
结论
service.getDetails() 应传入订阅的 product ID,即 goldmonthly1。goldplan1 是 base plan ID,表示该订阅商品的定价方案,不是独立的商品 SKU。
当前代码首先要修正错误处理。getDetails() 返回空数组时,代码不会渲染任何内容;抛出异常时,代码直接进入 catch 并切换 DIV,实际错误信息没有显示出来。使用 setTimeout() 还会让 JS interop 提前结束,定时器回调中的异常通常无法传回 Blazor。因此,没有看到 Blazor error 不代表调用已经成功。
显示实际返回结果
暂时不要渲染商品卡片,先把返回值、空结果和异常输出到页面及控制台:
window.checkDigitalGoodsApi = async () => {
const output = document.getElementById('divProductDetails');
if (!output) {
console.error('divProductDetails was not found.');
return;
}
output.textContent = '正在读取订阅信息……';
try {
if (!('getDigitalGoodsService' in window)) {
throw new Error('Digital Goods API is not supported.');
}
const service = await window.getDigitalGoodsService(
'https://play.google.com/billing'
);
const requestedIds = ['goldmonthly1'];
const details = await service.getDetails(requestedIds);
console.log('Requested product IDs:', requestedIds);
console.log('getDetails result:', details);
console.log('Serialized result:', JSON.stringify(details, null, 2));
if (!Array.isArray(details) || details.length === 0) {
output.textContent =
'getDetails() 调用成功,但没有返回商品。请检查商品、base plan、测试账号及发布状态。';
return;
}
output.replaceChildren();
for (const product of details) {
const section = document.createElement('section');
const title = document.createElement('h3');
const description = document.createElement('p');
const price = document.createElement('p');
title.textContent = product.title ?? product.itemId;
description.textContent = product.description ?? '';
if (product.price) {
price.textContent =
`Price: ${product.price.value} ${product.price.currency}`;
} else {
price.textContent = 'Price information is unavailable.';
}
section.append(title, description, price);
output.append(section);
}
} catch (error) {
console.error('Google Play Billing error:', error);
const name = error?.name ?? 'Error';
const message = error?.message ?? String(error);
output.textContent = `${name}: ${message}`;
}
};
还要注意,price 通常是一个包含 value 和 currency 的对象。直接使用 ${itmProduct.price},很可能只会得到 [object Object]。这与 title 不显示无关,但价格需要按对象字段读取。
在 Blazor 生命周期中调用
在 Blazor 中,适合调用这段 JavaScript 的位置是组件的 OnAfterRenderAsync。执行到这个生命周期时,组件的 DOM 已经生成:
@inject IJSRuntime JS
<div id="divGoogleDigitalGoodsAndBilling">
<div id="divProductDetails"></div>
</div>
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
await JS.InvokeVoidAsync("checkDigitalGoodsApi");
}
catch (JSException ex)
{
Console.Error.WriteLine(ex.Message);
}
}
}
}
这样可以删除 JavaScript 中的 setTimeout(),Promise rejection 也有机会通过 JS interop 传回 .NET。
如果确实需要延迟一个事件循环,应在同一个 Promise 链中等待:
await new Promise(resolve => setTimeout(resolve, 0));
不要把全部逻辑放进未被等待的 setTimeout(async () => ...) 回调。外层函数会在定时器执行前返回,之后发生的错误也无法正常传给 Blazor。
使用 goldmonthly1 还是 goldplan1
调用方式应为:
await service.getDetails(['goldmonthly1']);
这里使用的是订阅的 product ID:
goldmonthly1
goldplan1 是该订阅下的 base plan ID,用来描述续订周期、地区和价格等销售方案。如果单独传给 getDetails(),Google Play 会把它当作商品 ID 查询,通常返回空结果。
同一个订阅配置了多个 base plan 或 offer 时,还要确认当前 Digital Goods API、浏览器和 Google Play Billing 桥接层是否支持这套配置。排查时可以只保留一个已激活的 base plan,暂时移除复杂 offer,先确认基础商品能够返回。具体支持范围可能随 Chrome 和 Google Play Billing 更新而变化,应以项目当前运行环境对应的官方文档为准。
检查 Google Play Console 配置
能够获得 DigitalGoodsService,只能说明浏览器提供了该 API,并找到了相应的支付提供方,无法证明指定商品可以销售给当前用户。
需要检查以下配置:
- 订阅
goldmonthly1已创建并处于可用状态。 goldplan1已激活,没有停留在草稿或未完成状态。- base plan 已设置价格及可销售的国家或地区。
- 当前测试账号所在的国家或地区包含在销售范围内。
- Play Console 中的商品修改已经提交并生效。
- 当前安装包来自正确的 internal testing track,不是本地安装的 APK、旧版本,也不是在浏览器中直接访问的开发地址。
- 测试账号已加入对应测试轨道,并通过测试邀请链接加入测试。
- 测试购买流程时,账号还应按照 Play Console 的要求配置为 license tester。
- Play Console 中应用的 package name 与实际安装包一致。
- 如果应用通过 Trusted Web Activity 承载 PWA,应确认应用包、站点 origin 和 Digital Asset Links 对应当前发布版本。
- 设备上的 Chrome、Google Play Store 和 Google Play services 未使用旧版本,账号环境也没有异常。
Play Console 的配置变更可能无法立即同步到所有设备。不要预设具体等待时间,可以换用已经加入测试的账号或设备进行交叉验证。
区分异常和空数组
抛出异常与返回空数组通常指向不同的问题。
如果调用抛出异常:
try {
const details = await service.getDetails(['goldmonthly1']);
} catch (error) {
console.error(error.name, error.message, error.stack);
}
应检查运行环境、支付提供方、TWA 配置、账号状态和 API 支持情况。
如果调用成功,但返回空数组:
[]
应检查:
- 传入的是否为 product ID;
- 订阅及 base plan 是否已激活;
- 商品是否在当前地区销售;
- 当前账号是否属于测试轨道;
- 商品是否属于当前安装包对应的 Play Console 应用;
- 多 base plan 或 offer 是否超出当前桥接实现的支持范围。
原代码中的 for...of 遇到空数组时不会执行,也不会报错,所以页面会保持空白。这是出现“没有内容但也没有错误”的原因之一。
调试商店安装版本
从 Google Play 安装应用后,仍然可以尝试调试 JavaScript。对于由 Chrome 承载的 PWA 或 TWA,可按以下步骤操作:
- 在 Android 设备上开启开发者选项和 USB 调试。
- 使用 USB 将设备连接到开发电脑。
- 在桌面版 Chrome 打开:
chrome://inspect/#devices
- 找到对应页面、PWA 或 TWA,点击
Inspect。 - 查看 Console 和 Network,并在
getDetails()调用前后设置断点。
页面能否出现在远程调试列表中,取决于实际容器和调试配置。如果无法附加调试器,至少要在测试页面中显示 error.name、error.message、请求的 product ID 和返回数组长度。不要记录购买令牌、账号信息或其他敏感支付数据。
排查顺序
先处理静默 catch 和未等待的 setTimeout(),确定 getDetails() 是抛出异常还是返回空数组。接着固定使用 goldmonthly1,检查订阅和 goldplan1 是否均已激活,并确认已经配置当前地区的价格。之后再核对测试账号、internal testing 安装来源、package name 和 TWA 关联配置。
仅凭 getDigitalGoodsService 是否存在就显示“Google Play Billing is supported”并不准确。应等到服务创建和商品查询完成后,再判断当前环境能否正常使用该订阅商品。
备注:内容仅供参考。
