数字商品 API

社区组报告草案,

此版本:
https://wicg.github.io/digital-goods/
问题追踪:
GitHub
编辑:
(谷歌)
(谷歌)
参与:
GitHub WICG/digital-goods(新建问题,开放问题)
测试:
web-platform-tests digital-goods/(持续工作)

摘要

数字商品 API 允许 Web 应用程序获取其数字产品及其用户通过数字商店管理的购买信息。用户代理对商店的连接进行了抽象,并且使用 支付请求 API 进行购买。

本文档状态

本规范由 Web 平台孵化社区组 发布。 它不是 W3C 标准,也不属于 W3C 标准进程。 请注意,根据 W3C 社区贡献者许可协议(CLA) ,有有限的“选择退出”权利及其它适用条件。 了解更多关于 W3C 社区和商业组 的信息。

1. 使用示例

注意:本节为非规范性内容。

1.1. 获取服务实例

API 的使用始于调用 window.getDigitalGoodsService(), 该方法 可能仅在特定上下文下可用(如 HTTPS、应用、浏览器、操作系统等)。如果可用,可以传入服务提供商 URL 调用此方法。该方法 返回一个 promise,如果指定的服务提供商不可用则被拒绝。
if (window.getDigitalGoodsService === undefined) {
  // 当前上下文不支持数字商品 API。
  return;
}
try {
  const digitalGoodsService = await
      window.getDigitalGoodsService("https://example.com/billing");
  // 在此使用该服务。
  ...
} catch (error) {
  // 我们的首选服务提供商不可用。
  // 使用普通的网页支付流程。
  console.error("Failed to get service:", error.message);
  return;
}

1.2. 查询商品详情

const details = await digitalGoodsService
    .getDetails(['shiny_sword', 'gem', 'monthly_subscription']);
for (item of details) {
  const priceStr = new Intl.NumberFormat(
      locale,
      {style: 'currency', currency: item.price.currency}
    ).format(item.price.value);
  AddShopMenuItem(item.itemId, item.title, priceStr, item.description);
}

getDetails() 方法返回服务端关于给定商品集合的详情信息,目的是在菜单中展示给用户,让他们可以在无需实际进入购买流程前,先看到所有可购买项及其价格。

返回的 ItemDetails 序列可以是任意顺序,并且如果某商品在服务端不存在(即输入和输出不一定一一对应),也可能不会包含该商品。

商品 ID 是一个字符串,表示商品在商店服务器的主键。没有函数能获取商品 ID 列表,这些 ID 需硬编码在客户端代码中,或从开发者自己的服务器拉取。

商品定价为 PaymentCurrencyAmount ,包含当前用户地区与币种的价格设计,应使用 Intl.NumberFormat 进行本地化格式化,如上示例。

关于 ItemDetails 对象各字段的详细介绍,请参阅下方 [ItemDetails 字典] 部分。

1.3. 使用 Payment Request API 进行购买

const details = await digitalGoodsService.getDetails(['monthly_subscription']);
const item = details[0];
new PaymentRequest(
  [{supportedMethods: 'https://example.com/billing',
    data: {itemId: item.itemId}}]);

购买流程本身使用 Payment Request API。 这里只展示关键部分,未包含整个支付请求,但注意:用户选择购买的商品 ID 可以随特定支付方式,通过 methodData 条目的 data 字段传递给服务商。

1.4. 检查现有购买

purchases = await digitalGoodsService.listPurchases();
for (p of purchases) {
  VerifyOnBackendAndGrantEntitlement(p.itemId, p.purchaseToken);
}

listPurchases() 方法使客户端可以获取用户当前拥有或已购买的商品列表。常用于检查权益(如订阅/促销码/永久升级等是否激活)或在网络中断时恢复购买(如商品已购但尚未确认给后端)。返回的是商品 ID 和购买令牌,通常需通过开发者-服务商 API 校验后再授予权益。

1.5. 查询历史购买

const purchaseHistory = await digitalGoodsService.listPurchaseHistory();
for (p of purchaseHistory) {
  DisplayPreviousPurchase(p.itemId);
}

listPurchaseHistory() 方法可获取用户所有购买过的每种商品的最新记录,包含已过期或已消耗的商品。有些商店可能不保留历史,返回内容可能与 listPurchases() 相同。

1.6. 消耗购买记录

digitalGoodsService.consume(purchaseToken);

设计为可多次购买的商品,通常在再次购买前需先标记为“已消耗”。比如:游戏内道具(如临时增益)。可以使用 consume() 方法实现。

如条件允许,更建议使用开发者-服务商 API 直接消耗商品,以确保消耗的可验证性。

1.7. 与子域 iframe 搭配使用

<iframe
  src="https://sub.origin.example"
  allow="payment">
</iframe>

如需允许子域 iframe 调用数字商品 API,可在 iframe 元素上指定 allow 属性,并包含 "payment" 关键字。跨域 iframe 不可调用数字商品 API。更多细节见 Permissions Policy 规范。

2. API 定义

2.1. 对 Window 接口的扩展

partial interface Window {
  [SecureContext] Promise<DigitalGoodsService> getDigitalGoodsService(
      DOMString serviceProvider);
};

Window 对象可以暴露 getDigitalGoodsService() 方法。 不支持数字商品功能的用户代理不应在 Window 接口上暴露 getDigitalGoodsService() 方法。

注意: 上述说明是为了便于特性检测。如果 getDigitalGoodsService() 存在,可合理认为至少有一个服务提供商可用。

2.1.1. getDigitalGoodsService() 方法

注意: getDigitalGoodsService() 方法用于检测指定的 serviceProvider 是否支持 当前上下文。方法返回一个 Promise,如果支持则解析为 DigitalGoodsService 对象,否则如不支持或发生错误则拒绝。serviceProvider 通常为 基于 URL 的支付方式标识符。

当调用 getDigitalGoodsService(serviceProvider) 方法时,执行以下步骤:
  1. 令 document 为 当前设置对象的 相关全局对象的 关联 Document。

  2. 如果 document 不是 完全激活状态,则返回一个被拒绝的 promise,异常类型为 "InvalidStateError" DOMException。

  3. 如果 document 的源不满足同源要求,即与顶级源不同源,则返回一个拒绝理由为以下异常的 promise:"NotAllowedError" DOMException。

  4. 如果 document 无 允许使用 “payment” 权限,则返回一个被拒绝的 promise,异常类型为 "NotAllowedError" DOMException。

  5. 如果 serviceProvider 是 undefined 或 null 或空字符串,则返回一个被拒绝的 promise,异常类型为 TypeError。

  6. 令 result 为对 serviceProvider 和 document 执行 可创建数字商品服务算法 的结果。

  7. 如果 result 为 false,返回一个被拒绝的 promise,异常类型为 OperationError。

  8. 返回一个被解析的 promise,其值为新建的 DigitalGoodsService。

2.1.2. 可创建数字商品服务算法

可创建数字商品服务算法 检查 用户代理 是否支持指定的 serviceProvider 及 document 上下文。
  1. 用户代理 可以基于 serviceProvider、document 或外部因素返回 true 或返回 false。

注意: 允许用户代理针对不同服务商和不同上下文支持不同能力。

2.2. DigitalGoodsService 接口

[Exposed=Window, SecureContext] interface DigitalGoodsService {

  Promise<sequence<ItemDetails>> getDetails(sequence<DOMString> itemIds);

  Promise<sequence<PurchaseDetails>> listPurchases();

  Promise<sequence<PurchaseDetails>> listPurchaseHistory();

  Promise<undefined> consume(DOMString purchaseToken);
};

dictionary ItemDetails {
  required DOMString itemId;
  required DOMString title;
  required PaymentCurrencyAmount price;
  ItemType type;
  DOMString description;
  sequence<DOMString> iconURLs;
  DOMString subscriptionPeriod;
  DOMString freeTrialPeriod;
  PaymentCurrencyAmount introductoryPrice;
  DOMString introductoryPricePeriod;
  [EnforceRange] unsigned long long introductoryPriceCycles;
};

enum ItemType {
  "product",
  "subscription",
};

dictionary PurchaseDetails {
  required DOMString itemId;
  required DOMString purchaseToken;
};

2.2.1. getDetails() 方法

当调用 getDetails(itemIds) 方法时,执行下列步骤:
  1. 如果 itemIds 为空,则返回一个被拒绝的 promise,异常类型为 TypeError。

  2. 令 result 为从数字商品服务请求给定 itemIds 信息的结果。

注意: 允许用户代理针对不同数字商品服务提供商做不同的行为以支持多家服务商。

  1. 如果 result 为错误,则返回一个被拒绝的 promise,异常类型为 OperationError。

  2. 对于 result 中的每个 itemDetails:

    1. itemDetails.itemId 不应为空字符串。

    2. itemIds 应包含 itemDetails.itemId。

    3. itemDetails.title 不应为空字符串。

    4. itemDetails.price 必须为 规范 PaymentCurrencyAmount。

    5. 如有,itemDetails.subscriptionPeriod 必须为 iso-8601 持续时间。

    6. 如有,itemDetails.freeTrialPeriod 必须为 iso-8601 持续时间。

    7. 如有,itemDetails.introductoryPrice 必须为 规范 PaymentCurrencyAmount。

    8. 如有,itemDetails.introductoryPricePeriod 必须为 iso-8601 持续时间。

  3. 返回一个被解析的 promise,其值为 result。

注意: 并不要求 result 项的顺序与 itemIds 顺序一致。这样可允许缺失或无效项被跳过。

2.2.2. listPurchases() 方法

当调用 listPurchases() 方法时,执行下列步骤:
  1. 令 result 为从数字商品服务请求关于用户已购商品信息的结果。

注意: 允许用户代理针对不同数字商品服务提供商做不同行为以支持多家服务商。

  1. 如果 result 为错误,则返回一个被拒绝的 promise,异常类型为 OperationError。

  2. 对于 result 中的每个 itemDetails:

    1. itemDetails.itemId 不应为空字符串。

    2. itemDetails.purchaseToken 不应为空字符串。

  3. 返回一个被解析的 promise,其值为 result。

2.2.3. listPurchaseHistory() 方法

当调用 listPurchaseHistory() 方法时,执行下列步骤:
  1. 令 result 为请求用户购买过的每种商品类型最新购买信息的结果。

  2. 如果 result 为错误,则返回一个被拒绝的 promise,异常类型为 OperationError。

  3. 对于 result 中的每个 itemDetails:

    1. itemDetails.itemId 不应为空字符串。

    2. itemDetails.purchaseToken 不应为空字符串。

  4. 返回一个被解析的 promise,其值为 result。

2.2.4. consume() 方法

注意: 此处的“消耗”指用掉一次购买。消耗后用户将不再获得该权益。

当调用 consume(purchaseToken) 方法时,执行下列步骤:
  1. 若 purchaseToken 为空字符串,则返回一个被拒绝的 promise,异常类型为 TypeError。

  2. 令 result 为请求数字商品服务将 purchaseToken 标记为已消耗的结果。

注意: 允许用户代理根据实际服务商进行差异化实现。

  1. 如果 result 为错误,则返回一个被拒绝的 promise,异常类型为 OperationError。

  2. 返回一个被解析的 promise,其值为 undefined。

2.3. ItemDetails 字典

本节为非规范性内容。

ItemDetails 字典用于描述来自 serviceProvider 的数字商品详情。

2.4. PurchaseDetails 字典

本节为非规范性内容。

PurchaseDetails 字典表示用户曾购买过的来自 serviceProvider 的商品。

3. 权限策略集成

本规范定义了一个由字符串 "payment" 标识的 受策略控制的特性。 其 默认允许列表为 'self'。

注意: 文档 的 权限策略 决定本文件中任何内容是否允许获取 DigitalGoodsService 实例。若某文档中被禁用,则该文档内所有内容都不允许 调用 getDigitalGoodsService() 方法(尝试调用会抛出异常)。

4. 补充定义

"payment" 权限是 [permissions-policy] 特性, 参见 payment-request 规范 定义。

规范化的 PaymentCurrencyAmount 是指可以经过 PaymentCurrencyAmount 的 amount 字段在 校验并规范化金额 步骤处理后,不会抛出任何错误且不会被更改的值。

iso-8601 是一种日期时间格式标准。

一致性

文档约定

一致性要求以描述性断言与 RFC 2119 术语表达。 本规范中“必须(MUST)”、“不得(MUST NOT)”、“必需(REQUIRED)”、“应(SHOULD)”、“可(MAY)”等关键字参考 RFC 2119 解释。 不过为了更易读,规范未全部采用大写标注。

标准正文外除明确标记为非规范性、示例与注释的部分外,其余均为强制性内容。[RFC2119]

规范中的示例通过“for example”引入或单独使用 class="example" 标示,如下所示:

这是一条说明性示例。

说明性注释以 Note 开头且采用 class="note",如:

Note, 这是说明性注释。

一致性算法

算法描述中的命令性表述(如“移除所有前导空格”或“返回 false 并终止流程”) 应结合引入算法时的关键字("must", "should", "may" 等)理解。

以算法或步骤表达的规范要求,可以用任何实现方式达成,只要最终效果等价即可。 本规范中的算法旨在说明其含义而非追求高性能,鼓励实现者针对性能优化。

索引

本规范定义的术语

引用定义的术语

参考文献

规范性引用

[HTML]
Anne van Kesteren 等. HTML 标准. 实时标准. 链接: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 标准. 实时标准. 链接: https://infra.spec.whatwg.org/
[PAYMENT-REQUEST]
Marcos Caceres; Rouslan Solomakhin; Ian Jacobs. 支付请求 API(Payment Request API). 链接: https://w3c.github.io/payment-request/
[PERMISSIONS-POLICY]
Ian Clelland. 权限策略(Permissions Policy). 链接: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. RFCs中用于指明需求级别的关键词. 1997年3月. 最佳当前实践. 链接: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 标准. 实时标准. 链接: https://webidl.spec.whatwg.org/

补充性引用

[DOM]
Anne van Kesteren. DOM 标准. 实时标准. 链接: https://dom.spec.whatwg.org/
[PAYMENT-METHOD-ID]
Marcos Caceres. 支付方式标识符(Payment Method Identifiers). 链接: https://w3c.github.io/payment-method-id/

IDL 索引

partial interface Window {
  [SecureContext] Promise<DigitalGoodsService> getDigitalGoodsService(
      DOMString serviceProvider);
};

[Exposed=Window, SecureContext] interface DigitalGoodsService {

  Promise<sequence<ItemDetails>> getDetails(sequence<DOMString> itemIds);

  Promise<sequence<PurchaseDetails>> listPurchases();

  Promise<sequence<PurchaseDetails>> listPurchaseHistory();

  Promise<undefined> consume(DOMString purchaseToken);
};

dictionary ItemDetails {
  required DOMString itemId;
  required DOMString title;
  required PaymentCurrencyAmount price;
  ItemType type;
  DOMString description;
  sequence<DOMString> iconURLs;
  DOMString subscriptionPeriod;
  DOMString freeTrialPeriod;
  PaymentCurrencyAmount introductoryPrice;
  DOMString introductoryPricePeriod;
  [EnforceRange] unsigned long long introductoryPriceCycles;
};

enum ItemType {
  "product",
  "subscription",
};

dictionary PurchaseDetails {
  required DOMString itemId;
  required DOMString purchaseToken;
};