디지털 상품 API

초안 커뮤니티 그룹 보고서,

이 버전:
https://wicg.github.io/digital-goods/
이슈 추적:
GitHub
에디터:
(Google)
(Google)
참여:
GitHub WICG/digital-goods (새 이슈, 열린 이슈)
테스트:
web-platform-tests digital-goods/ (진행 중)

요약

디지털 상품 API는 웹 애플리케이션이 디지털 상품 및 해당 사용자의 디지털 스토어로 관리되는 구매 정보에 접근할 수 있도록 해줍니다. 사용자 에이전트는 스토어와의 연결을 추상화하며, 결제 요청 API(Payment Request API)를 이용해 구매가 이루어집니다.

이 문서의 현황

이 명세는 웹 플랫폼 인큐베이터 커뮤니티 그룹(Web Platform Incubator Community Group)에 의해 공개되었습니다. W3C 표준이 아니며 W3C 표준 트랙에도 포함되어 있지 않습니다. W3C 커뮤니티 기여자 라이선스 동의서(CLA)에 따라 제한적 옵트아웃 등 조건이 있을 수 있습니다. W3C 커뮤니티 및 비즈니스 그룹에 대해 더 알아보세요.

1. 사용 예시

참고: 본 섹션은 규범적이지 않습니다.

1.1. 서비스 인스턴스 얻기

API 사용은 window.getDigitalGoodsService() 호출로 시작되며, 이 방법은 특정 컨텍스트(예: HTTPS, 앱, 브라우저, OS)에서만 제공될 수 있습니다. 만약 지원된다면, 서비스 제공자 URL과 함께 메서드를 호출할 수 있습니다. 이 메서드는 주어진 서비스 제공자를 사용할 수 없는 경우 promise를 reject로 반환합니다.
if (window.getDigitalGoodsService === undefined) {
  // 이 환경에서는 Digital Goods 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 시퀀스의 순서는 임의이며, 서버에 존재하지 않는 항목은 포함하지 않을 수 있습니다(즉, 입력과 출력이 1:1로 일치하지 않을 수 있음).

항목 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를 사용합니다. 전체 결제 요청 코드는 생략하였으나, 사용자가 선택한 항목의 item ID를 해당 결제 메서드의 methodDatadata 필드에 상점별 형식으로 넘길 수 있습니다.

1.4. 기존 구매내역 확인

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

listPurchases() 메서드는 사용자가 현재 소유 중이거나 결제한 아이템들의 목록을 클라이언트가 조회할 수 있게 해줍니다. 권한(예: 구독, 프로모션 코드, 영구 업그레이드 등)이 활성 상태인지 확인하거나, 구매 도중 네트워크 오류 발생 시(예: 구매는 되었으나 백엔드 확인이 안 된 상태) 복구 목적으로 사용할 수 있습니다. 반환값에는 item ID 및 구매 토큰이 포함되며, 권한 부여 전에 개발자와 제공자 간의 직접 API로 검증할 수 있습니다.

1.5. 과거 구매내역 확인

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

listPurchaseHistory() 메서드는 사용자가 지금까지 구매한 각 항목 타입의 최근 구매내역을 조회할 수 있도록 합니다. 만료되었거나 소비된 구매도 포함할 수 있습니다. 일부 상점에서는 이 내역을 보관하지 않을 수 있으며, 그런 경우 listPurchases()와 같은 데이터를 반환할 수 있습니다.

1.6. 구매 소비 처리

digitalGoodsService.consume(purchaseToken);

여러 번 구매할 수 있는 상품은 일반적으로 사용자가 다시 구매하기 전에 "소비됨(consumed)"으로 표시해야 합니다. 예를 들어, 일시적으로 플레이어를 강하게 해주는 게임 내 파워업은 소비성 구매의 한 예입니다. 이러한 처리는 consume() 메서드를 통해 할 수 있습니다.

더 확실하게 구매가 소진되었음을 검증하려면, 가능시 직접 제공자의 API를 통해 소비 처리를 권장합니다.

1.7. 서브도메인 iframe에서 사용

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

서브도메인 iframe에서 Digital Goods API의 사용을 허용하려면, iframe 요소에 allow 속성과 "payment" 키워드를 추가해야 합니다. 교차 출처(Cross-origin) iframe에서는 Digital Goods API를 사용할 수 없습니다. 보다 자세한 내용과 예시는 Permissions Policy 명세를 참고하세요.

2. API 정의

2.1. Window 인터페이스 확장

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

Window 오브젝트는 getDigitalGoodsService() 메서드를 노출할 수 있습니다. Digital Goods를 지원하지 않는 유저 에이전트는 getDigitalGoodsService()Window 인터페이스에 노출하면 안 됩니다.

참고: 위 설명은 기능 감지를 허용하기 위한 것입니다. getDigitalGoodsService() 가 존재한다면, 최소한 하나의 서비스 제공자와 동작할 수 있다고 합리적으로 기대할 수 있습니다.

2.1.1. getDigitalGoodsService() 메서드

참고: getDigitalGoodsService() 메서드는 주어진 serviceProvider 가 현재 컨텍스트에서 지원되는지 확인하기 위해 호출됩니다. 이 메서드는 Promise를 반환하고, 지원 시 DigitalGoodsService 오브젝트로 resolve되거나, 지원하지 않거나 에러가 발생하면 reject로 반환됩니다. serviceProvider 는 일반적으로 url 기반 결제 메서드 식별자입니다.

getDigitalGoodsService(serviceProvider) 메서드가 호출되면, 다음 순서대로 동작합니다:
  1. document현재 설정 객체관련 글로벌 객체연결된 Document로 둔다.

  2. document완전히 활성(fully active) 상태가 아니라면 거부된 promise"InvalidStateError" DOMException을 반환한다.

  3. documentorigin상위 Origin과 동일하지 않으면 거부된 promise"NotAllowedError" DOMException을 반환한다.

  4. document가 "payment" 권한을 허용되지 않은 경우, 거부된 promise"NotAllowedError" DOMException을 반환한다.

  5. serviceProvider가 undefined이거나 null, 빈 문자열일 경우 거부된 promiseTypeError을 반환한다.

  6. resultcan make digital goods service algorithmserviceProviderdocument로 수행한 결과로 둔다.

  7. result가 false면 거부된 promiseOperationError를 반환한다.

  8. resolve된 promise와 새로운 DigitalGoodsService를 반환한다.

2.1.2. Digital Goods Service 생성 가능 알고리즘

can make digital goods service algorithmuser agent가 특정 serviceProviderdocument 컨텍스트를 지원하는지 확인합니다.
  1. user agentserviceProvider, 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비어 있다면, 거부된 promiseTypeError를 반환합니다.

  2. result를 디지털 상품 서비스에서 itemIds에 대한 정보를 요청한 결과로 둡니다.

참고: 이를 통해 사용자 에이전트에서 서비스 제공자별 동작으로 다양한 디지털 상품 제공자를 지원할 수 있습니다.

  1. 만약 result가 에러라면, 거부된 promiseOperationError를 반환합니다.

  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. resolve된 promiseresult를 반환합니다.

참고: result의 항목 순서가 itemIds와 일치해야 할 필요는 없습니다. 이는 누락되거나 유효하지 않은 항목이 출력 목록에서 건너뛸 수 있도록 허용하기 위함입니다.

2.2.2. listPurchases() 메서드

listPurchases() 메서드가 호출되면, 아래 단계를 따릅니다:
  1. result를 디지털 상품 서비스에서 사용자의 구매 정보 요청 결과로 둡니다.

참고: 이를 통해 사용자 에이전트에서 서비스 제공자별 동작으로 다양한 디지털 상품 제공자를 지원할 수 있습니다.

  1. 만약 result가 에러라면, 거부된 promiseOperationError를 반환합니다.

  2. result의 각 itemDetails에 대하여:

    1. itemDetails.itemId는 빈 문자열이 아니어야 합니다.

    2. itemDetails.purchaseToken은 빈 문자열이 아니어야 합니다.

  3. resolve된 promiseresult를 반환합니다.

2.2.3. listPurchaseHistory() 메서드

listPurchaseHistory() 메서드가 호출되면, 아래 단계를 따릅니다:
  1. result를 사용자가 지금까지 구매한 각 항목 타입의 최신 구매 정보 요청 결과로 둡니다.

  2. 만약 result가 에러라면, 거부된 promiseOperationError를 반환합니다.

  3. result의 각 itemDetails에 대하여:

    1. itemDetails.itemId는 빈 문자열이 아니어야 합니다.

    2. itemDetails.purchaseToken은 빈 문자열이 아니어야 합니다.

  4. resolve된 promiseresult를 반환합니다.

2.2.4. consume() 메서드

참고: 여기서 consume은 구매 내용을 소진(사용)하는 것을 의미합니다. 소비가 완료되면 사용자는 더 이상 해당 구매에 대해 권리가 없습니다.

consume(purchaseToken) 메서드가 호출되면, 아래 단계를 따릅니다:
  1. purchaseToken이 빈 문자열이면, 거부된 promiseTypeError를 반환합니다.

  2. result를 디지털 상품 서비스에 purchaseToken을 소비 처리하라고 요청한 결과로 둡니다.

참고: 이를 통해 사용자 에이전트에서 서비스 제공자별 동작으로 다양한 디지털 상품 제공자를 지원할 수 있습니다.

  1. 만약 result가 에러라면, 거부된 promiseOperationError를 반환합니다.

  2. resolve된 promiseundefined를 반환합니다.

2.3. ItemDetails 딕셔너리

이 섹션은 규범적이지 않습니다.

ItemDetails 딕셔너리는 serviceProvider 로부터 전달받은 디지털 아이템 정보를 나타냅니다.

2.4. PurchaseDetails 딕셔너리

이 섹션은 규범적이지 않습니다.

PurchaseDetails 딕셔너리는 사용자가 serviceProvider 로부터 구입한 디지털 아이템 정보를 나타냅니다.

3. Permissions Policy 통합

이 명세는 "정책 제어 기능"을 "payment" 문자열로 정의합니다. 기본 허용 목록은 'self'입니다.

참고: 문서(document)permissions policy는 해당 문서의 모든 콘텐츠가 DigitalGoodsService 인스턴스를 획득할 수 있는지 결정합니다. 비활성화된 경우, 해당 문서 내의 어떤 콘텐츠도 사용할 수 없으며, getDigitalGoodsService() 호출 시 예외가 발생합니다.

4. 추가 정의

"payment" 권한은 [permissions-policy] 기능이며, payment-request 명세에서 정의됩니다.

정규 PaymentCurrencyAmountPaymentCurrencyAmount amount에 대해 체크 및 정규화 과정을 거쳐 오류가 발생하지 않고 값이 바뀌지 않는 경우를 의미합니다.

iso-8601은 날짜와 시간 형식 표준입니다.

준수

문서 규칙

준수 요건은 설명적 주장과 RFC 2119 용어의 조합으로 표현됩니다. 본 문서의 규범적 부분에서 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, “OPTIONAL” 등 핵심 단어는 RFC 2119에 정의된 의미로 해석되어야 합니다. 단, 가독성을 위해 명세 내에서 항상 대문자로 쓰이지는 않습니다.

본 명세의 모든 내용은 별도로 명시적으로 '비규범적'임을 표시하거나, 예시, 노트가 아닌 한 적용됩니다. [RFC2119]

본 명세의 예시는 “예를 들어(for example)”라는 표현으로 시작하거나, 또는 class="example"와 같이 규범적 텍스트와 구분되어 작성됩니다. 예:

이것은 참고용 예시입니다.

참고 노트는 “Note”라는 단어로 시작하며, class="note"로 구분되어 아래와 같이 표기됩니다:

Note, 이 내용은 참고용입니다.

준수 알고리즘

알고리즘 내에서 사용된 명령형 구문(예: "선행 공백을 모두 삭제한다" 또는 "이 단계에서 false를 반환하고 중단한다")에 포함된 단어("must", "should", "may" 등)는 알고리즘의 도입부에서 사용된 의미에 따라 해석되어야 합니다.

알고리즘이나 구체적인 단계로 표현된 준수 요건은, 그 결과가 동등하다면 어떤 방식으로든 구현할 수 있습니다. 특히 본 명세에 정의된 알고리즘은 구현이 쉽도록 작성되어 있으나, 반드시 성능에 초점을 맞춘 것은 아닙니다. 구현자는 최적화를 권장합니다.

색인

이 명세에 정의된 용어

참고로 정의된 용어

참고 문헌

규범적 참고 문헌

[HTML]
Anne van Kesteren; 외. HTML 표준. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 표준. Living Standard. URL: https://infra.spec.whatwg.org/
[PAYMENT-REQUEST]
Marcos Caceres; Rouslan Solomakhin; Ian Jacobs. Payment Request API. URL: https://w3c.github.io/payment-request/
[PERMISSIONS-POLICY]
Ian Clelland. Permissions Policy. URL: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. RFC에서 요구 사항 수준을 나타내기 위한 핵심 단어. 1997년 3월. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 표준. Living Standard. URL: https://webidl.spec.whatwg.org/

참고용 참고 문헌

[DOM]
Anne van Kesteren. DOM 표준. Living Standard. URL: https://dom.spec.whatwg.org/
[PAYMENT-METHOD-ID]
Marcos Caceres. 결제 메서드 식별자. URL: 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;
};