跳转到内容

能力 · API

Digital Goods API

发布于 更新于

Digital Goods API 让 Web 应用从商店后端读取自己的商品目录和用户的购买记录:window.getDigitalGoodsService(provider) 返回一个 DigitalGoodsService,其 getDetails()、listPurchases()、listPurchaseHistory() 与 consume() 直接对接该提供方。购买本身通过 Payment Request API 完成,付款方式用的是同一个提供方 URL。

Android 与 ChromeOS 上的 Chrome 101 是唯一实现,并且只在 PWA 运行于发布到 Google Play 的 Trusted Web Activity 中、以 https://play.google.com/billing 为提供方时可用(Chrome for Developers 指南)。MDN 的 browser-compat-data 没有 DigitalGoodsService 条目;Firefox 与 Safari 不暴露 getDigitalGoodsService。规范是 WICG 草案,不支持的环境下 Window 上根本没有这个方法,所以一个 in 检查就是完整的特性检测。

window.getDigitalGoodsService(serviceProvider)
digitalGoodsService.getDetails(itemIds)
digitalGoodsService.listPurchases()
digitalGoodsService.listPurchaseHistory()
digitalGoodsService.consume(purchaseToken)

getDigitalGoodsService() 返回 Promise<DigitalGoodsService>。getDetails() 返回 Promise<sequence<ItemDetails>>,listPurchases() 与 listPurchaseHistory() 返回 Promise<sequence<PurchaseDetails>>,consume() 返回 Promise<undefined>。一切都是 [SecureContext] 且仅限 Window;没有方法需要用户手势,但文档必须与顶层页面同源,并被允许使用 payment 这个 Permissions Policy 特性(同站 iframe 需要 allow="payment")。

三个方法接受参数;两个列表方法不接受。

参数 类型 必填 说明
serviceProvider DOMString 是 基于 URL 的支付方式标识符;Google Play 为 "https://play.google.com/billing"。undefined、null 或 "" 会拒绝。
itemIds sequence<DOMString> 是 在商店控制台配置的商品或订阅 ID。不能为空;结果可能省略未知 ID,顺序也不保证。
purchaseToken DOMString 是 listPurchases() 或购买时 PaymentResponse.details 返回的令牌。不能为空。

getDetails() 兑现的 ItemDetails 字典包含以下成员:

成员 类型 必填 说明
itemId DOMString 是 被请求的 ID。
title DOMString 是 用于展示的本地化名称。
price PaymentCurrencyAmount 是 用户所在地区的 { currency, value };用 Intl.NumberFormat 格式化。
type "product" 或 "subscription" 否 商品种类。
description DOMString 否 本地化的详细描述。
iconURLs sequence<DOMString> 否 商品图片。
subscriptionPeriod DOMString 否 ISO 8601 时长,仅订阅。
freeTrialPeriod DOMString 否 免费试用的 ISO 8601 时长。
introductoryPrice PaymentCurrencyAmount 否 优惠期内的价格。
introductoryPricePeriod DOMString 否 优惠期的 ISO 8601 时长。
introductoryPriceCycles unsigned long long 否 优惠价适用的计费周期数。

listPurchases() 与 listPurchaseHistory() 兑现的 PurchaseDetails 字典有两个必填成员 itemId 与 purchaseToken;listPurchaseHistory() 可能包含已消耗或已过期的购买,没有历史记录的商店返回与 listPurchases() 相同的列表。

每个方法都以拒绝而非抛出报错。Chromium 的 OperationError 消息就是 Play Billing 的响应码字面量,这是区分「商品已拥有」与「商店应用缺失」的唯一途径。

异常 条件
InvalidStateError 在非 fully active 的文档中调用 getDigitalGoodsService()。
NotAllowedError 调用 getDigitalGoodsService() 的文档与顶层源不同源,或 payment Permissions Policy 不允许。
TypeError getDigitalGoodsService() 的提供方为 undefined、null 或空串;getDetails([]);consume("")。
OperationError getDigitalGoodsService() 的提供方在当前环境不受支持(不在 TWA 内,或打包时未启用 Play Billing);四个服务方法在商店报错时。Chromium 的消息字符串:unsupported payment method、unsupported context、error、itemAlreadyOwned、itemNotOwned、itemUnavailable、clientAppUnavailable、clientAppError。

针对同一提供方的 PaymentRequest.show() 在用户退出 Play 购买面板时以 AbortError 拒绝;那是 Payment Request API 的异常,不属于本 API。

每个示例都从同一个辅助函数出发,它在 Play 支撑的 Trusted Web Activity 之外兑现 null,应用的其余部分据此显示网页收银台(Stripe、PayPal 或普通表单)。

获取 Play 服务,不可用时回退到网页收银台

Section titled “获取 Play 服务,不可用时回退到网页收银台”

in 检查只说明浏览器实现了该 API;只有 Promise 兑现才说明这个安装包能连上 Play Billing。两种失败按同一方式处理。

const PLAY_BILLING = 'https://play.google.com/billing';
async function getPlayService() {
if (!('getDigitalGoodsService' in window)) return null;
try {
return await window.getDigitalGoodsService(PLAY_BILLING);
} catch (err) {
console.info(`Play Billing 不可用(${err.name}: ${err.message})`);
return null;
}
}
const service = await getPlayService();
document.querySelector('#web-checkout').hidden = Boolean(service);
document.querySelector('#play-checkout').hidden = !service;

记录 err.message 能把 Play 响应码(unsupported context、clientAppUnavailable)留在遥测里,而不改变用户看到的内容。

用 getDetails() 按用户区域渲染价格

Section titled “用 getDetails() 按用户区域渲染价格”

price 以 PaymentCurrencyAmount 形式到达;用 Intl.NumberFormat 格式化,而不是自己拼接 value 与 currency。缺失的 ID 直接不在结果里,所以遍历响应而不是请求。

async function renderCatalogue(service, ids) {
if (!service) return renderWebPrices(ids); // 从你自己的服务器获取
const items = await service.getDetails(ids);
const money = new Intl.NumberFormat(navigator.language, { style: 'currency', currency: items[0]?.price.currency ?? 'USD' });
for (const item of items) {
const row = document.createElement('li');
row.textContent = `${item.title}: ${money.format(item.price.value)}`;
if (item.type === 'subscription' && item.freeTrialPeriod) {
row.textContent += `(试用 ${item.freeTrialPeriod})`;
}
document.querySelector('#catalogue').append(row);
}
}

P7D 之类的试用期是 ISO 8601 时长字符串,需要自行格式化;Chrome 101 的 Intl 没有可用于它的时长格式化器。

用 PaymentRequest 购买,再用 listPurchases() 恢复权益

Section titled “用 PaymentRequest 购买,再用 listPurchases() 恢复权益”

购买流程是 Payment Request:supportedMethods 设为提供方 URL,SKU 放进 data;Play 忽略你仍必须提供的 total。在服务器上确认 purchaseToken:任何三天内未确认的购买,Google Play 都会退款并撤销(Chrome for Developers 指南)。之后的启动里,listPurchases() 返回用户已拥有的内容,包括在其他设备上的购买。

async function buy(service, sku) {
if (!service) return startWebCheckout(sku);
const request = new PaymentRequest(
[{ supportedMethods: PLAY_BILLING, data: { sku } }],
{ total: { label: 'Total', amount: { currency: 'USD', value: '0' } } },
);
try {
const response = await request.show();
const { purchaseToken } = response.details;
const verified = await postJson('/play/acknowledge', { sku, purchaseToken });
await response.complete(verified ? 'success' : 'fail');
if (verified) await service.consume(purchaseToken); // 仅限消耗型商品
} catch (err) {
if (err.name !== 'AbortError') throw err; // 用户退出了 Play 购买面板
}
}
async function restoreEntitlements(service) {
if (!service) return [];
const owned = await service.listPurchases();
return (await postJson('/play/verify', owned)).filter((p) => p.valid).map((p) => p.itemId);
}

consume() 只用于设计为可重复购买的商品:规范写明购买被消耗后用户预期不再享有该权益,所以不要对订阅或永久升级调用它。

规范

规范状态
Digital Goods API: getDigitalGoodsService() methodWICG 草案