Blazor WASM PWA 无法获取 Google Play Billing 订阅详情
结论
getDetails() 应传入订阅的 product ID:
await service.getDetails(['goldmonthly1']);
goldplan1 是 base plan ID,通常不能作为独立商品 ID 传给 getDetails()。Google Play 会根据已启用的 base plan、offer、地区和测试账号资格等信息,决定具体采用哪个 base plan。
目前最需要处理的不是 setTimeout,而是代码吞掉了 getDetails() 抛出的所有异常。无论是商品配置、账号资格、发布状态,还是返回数据的处理出了问题,都会进入同一个 catch。由于页面和控制台都没有输出实际异常,最终看起来就像“没有任何内容”。
先显示真实错误
不要用一个没有诊断信息的 catch 包住整个流程。至少应记录错误名称、消息和堆栈,并把结果显示在页面上:
window.checkDigitalGoodsApi = async () => {
const billingDiv = document.getElementById(
'divGoogleDigitalGoodsAndBilling'
);
const digitalGoodsOnlyDiv = document.getElementById(
'divGoogleDigitalGoodsOnly'
);
const neitherDiv = document.getElementById('divNeither');
const productDetailsDiv = document.getElementById('divProductDetails');
const showError = (message) => {
console.error(message);
if (productDetailsDiv) {
productDetailsDiv.innerHTML =
`<pre style="white-space: pre-wrap; color: red;"></pre>`;
productDetailsDiv
.querySelector('pre')
.textContent = message;
}
};
if (!('getDigitalGoodsService' in window)) {
billingDiv.hidden = true;
digitalGoodsOnlyDiv.hidden = true;
neitherDiv.hidden = false;
return;
}
let service;
try {
service = await window.getDigitalGoodsService(
'https://play.google.com/billing'
);
} catch (error) {
billingDiv.hidden = true;
digitalGoodsOnlyDiv.hidden = false;
neitherDiv.hidden = true;
console.error('getDigitalGoodsService failed:', error);
return;
}
billingDiv.hidden = false;
digitalGoodsOnlyDiv.hidden = true;
neitherDiv.hidden = true;
try {
const items = await service.getDetails(['goldmonthly1']);
console.log('getDetails result:', items);
console.log('Serialized result:', JSON.stringify(items, null, 2));
if (!items || items.length === 0) {
showError(
'getDetails() returned no products for product ID "goldmonthly1".'
);
return;
}
productDetailsDiv.replaceChildren();
for (const item of items) {
const container = document.createElement('section');
const title = document.createElement('h3');
const description = document.createElement('p');
const price = document.createElement('p');
title.textContent = item.title ?? item.itemId ?? 'Subscription';
description.textContent = item.description ?? '';
/*
* Digital Goods API 的 price 通常是结构化金额,不一定是字符串。
* 先查看实际返回对象,再按当前运行环境的字段处理。
*/
price.textContent =
`Price: ${formatDigitalGoodsPrice(item.price)}`;
container.append(title, description, price);
productDetailsDiv.append(container);
}
} catch (error) {
const message = [
'service.getDetails() failed.',
`name: ${error?.name ?? 'UnknownError'}`,
`message: ${error?.message ?? String(error)}`,
error?.stack ? `stack: ${error.stack}` : ''
].filter(Boolean).join('\n');
showError(message);
}
};
function formatDigitalGoodsPrice(price) {
if (price == null) {
return 'Unavailable';
}
if (typeof price === 'string') {
return price;
}
if (price.currency && price.value != null) {
try {
return new Intl.NumberFormat(undefined, {
style: 'currency',
currency: price.currency
}).format(Number(price.value));
} catch {
// 继续使用原始值
}
}
return JSON.stringify(price);
}
这段代码也避免了把服务端返回的标题和描述直接写入 innerHTML。改用 textContent 更安全,内容中的特殊字符也不会破坏 HTML。
检查 Google Play 中的订阅配置
成功取得 DigitalGoodsService,只能说明当前容器支持 Google Play Billing,不能证明当前用户一定能查询到指定商品。
需要逐项确认:
goldmonthly1是订阅的 product ID,不是后台显示名称。- 订阅处于可用状态,而不是草稿或已停用状态。
goldplan1base plan 已启用,并设置了有效价格。- base plan 对测试设备所在的国家或地区可用。
- 当前安装包的 application ID/package name 与 Play Console 中的应用一致。
- PWA/TWA 的站点来源、应用包和 Digital Asset Links 关联正确。
- 测试设备上的 Google Play 登录账号已加入对应的内部测试轨道,并接受了测试邀请。
- 应用通过 Google Play 的测试链接安装,而不是通过本地 APK、浏览器直接访问或其他来源启动。
- 当前设备上的应用版本属于配置了该订阅的正确 Play Console 应用。
- Play Console 中刚完成的商品或测试轨道变更可能还没有传播完成。遇到这种情况,只能稍后重新安装或重试,不能仅根据 API 可用就判断配置已经生效。
即使应用有多个 base plan,getDetails(['goldmonthly1']) 查询的仍是订阅商品。不要假定返回对象一定包含某个指定 base plan 的全部后台字段。Digital Goods API 提供的是经过浏览器统一处理的商品信息,不等同于原生 Play Billing Library 的完整 ProductDetails 模型。
不需要依赖 setTimeout(..., 0)
setTimeout(..., 0) 只是把回调推迟到后续事件循环,不能可靠地保证 Blazor 已完成目标组件的渲染。更稳妥的做法是在组件首次渲染完成后调用 JavaScript:
@inject IJSRuntime JS
<div id="divGoogleDigitalGoodsAndBilling" hidden>
<div id="divProductDetails"></div>
</div>
<div id="divGoogleDigitalGoodsOnly" hidden>
<h2>Subscriptions</h2>
<p>Digital Goods API is available but Google Play Billing is not supported!</p>
</div>
<div id="divNeither" hidden>
<h2>Subscriptions</h2>
<p>
For the fully-featured app with paid subscriptions,
you will need the Google Play store version for Android.
</p>
</div>
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("checkDigitalGoodsApi");
}
}
}
此时 DOM 已完成首次渲染,JavaScript 不再需要 setTimeout。异步回调写成 async () => {} 本身没有问题,问题是 timeout 不能可靠地与 Blazor 生命周期同步。
如何调试 Play Store 安装的版本
“必须通过 Google Play 安装”并不意味着“无法调试”。对于运行在 Chrome、WebView 或 TWA 中的应用,通常可以尝试使用 Android 远程调试:
- 在 Android 设备上启用开发者选项和 USB 调试。
- 使用 USB 将设备连接到电脑。
- 在桌面版 Chrome 中打开:
chrome://inspect/#devices
- 启动从 Google Play 安装的测试版本。
- 在可检查目标中找到对应页面,点击
inspect。 - 查看 Console 和 Network,并检查:
await window.getDigitalGoodsService('https://play.google.com/billing')
以及:
await service.getDetails(['goldmonthly1'])
能否找到可检查目标,取决于实际使用的 Chrome、WebView/TWA 容器及其调试配置。如果发布构建没有出现在 chrome://inspect 中,页面内的错误输出仍然有用。也可以暂时把 JSON.stringify(items, null, 2) 和异常消息显示在测试页面上。
不要只记录 ${error}。部分 DOM 异常经过这种转换后只能得到很少的信息,应分别记录:
console.error(error);
console.error(error?.name);
console.error(error?.message);
console.error(error?.stack);
正确的排查顺序是:先用 goldmonthly1 查询,再输出真实异常和原始返回值,然后确认订阅及 base plan 的可售状态,最后通过远程 DevTools 或页面内诊断,判断问题究竟是商品不可见、调用被拒绝,还是返回字段的处理方式不正确。
备注:内容仅供参考。
