@mpgd/platform 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commerce.js CHANGED
@@ -6,7 +6,7 @@ import * as _accessExpressionAsString_1 from "typia/lib/internal/_accessExpressi
6
6
  const __typia_transform__accessExpressionAsString = _accessExpressionAsString_1._accessExpressionAsString;
7
7
  import typia from 'typia';
8
8
  import { PlatformOperationError, readPlatformOperationFailure, } from './index.js';
9
- export { createUnsupportedCommerceCapabilities, deriveLegacyCommerceCapabilities, } from './commerce-capabilities.js';
9
+ export { createUnsupportedCommerceCapabilities } from './commerce-capabilities.js';
10
10
  /**
11
11
  * Commerce v2 client contract. Pure types, validators, and helpers only: no
12
12
  * provider lives here, and nothing in this module grants a purchase. The
@@ -39,6 +39,10 @@ export const commerceBridgeMethods = Object.freeze([
39
39
  'commerce.v2.finishTransaction',
40
40
  'commerce.v2.restorePurchases',
41
41
  ]);
42
+ // `satisfies` only rejects an unknown name; this rejects a missing one, so a
43
+ // method added to `CommerceBridgeMethods` cannot be left out of the list.
44
+ const commerceBridgeMethodListIsExhaustive = true;
45
+ void commerceBridgeMethodListIsExhaustive;
42
46
  // Errors.
43
47
  const retryableCommerceErrorCodes = new Set([
44
48
  'NETWORK',
@@ -204,8 +208,9 @@ export function assertRequestPurchaseSupported(capabilities, input, kind) {
204
208
  * the same key: `store:<storeId>:<transactionId>` by default, with each
205
209
  * component percent-encoded (`encodeURIComponent`) so ids containing `:`
206
210
  * cannot collide, and the forms v1 ledgers already use for Google Play
207
- * (`play-token-sha256:<hex>`) and Apps in Toss (`apps-in-toss:purchase:<orderId>`),
208
- * which stay byte-identical.
211
+ * (`play-token-sha256:<hex>`) and Apps in Toss
212
+ * (`apps-in-toss:purchase:<encodeURIComponent(orderId)>`), which stay
213
+ * byte-identical.
209
214
  */
210
215
  export async function createDerivedPurchaseIdempotencyKey(input) {
211
216
  const store = readIdentifier(input.store, 128);
@@ -220,11 +225,16 @@ export async function createDerivedPurchaseIdempotencyKey(input) {
220
225
  throw new TypeError('Derived purchase keys require a transaction id.');
221
226
  }
222
227
  if (store === 'apps-in-toss') {
223
- return `apps-in-toss:purchase:${transactionId}`;
228
+ // Byte for byte the v1 `createAppsInTossPurchaseIdempotencyKey` form.
229
+ return `apps-in-toss:purchase:${encodeURIComponent(transactionId)}`;
224
230
  }
225
231
  return `store:${encodeURIComponent(store)}:${encodeURIComponent(transactionId)}`;
226
232
  }
227
- /** Byte-for-byte the v1 `createGooglePlayTokenTransactionId` form; A4 replaces the two copies with this helper. */
233
+ /**
234
+ * The `play-token-sha256:<hex>` idempotency key of a Google Play purchase
235
+ * token. The one shared helper: the plugin and `@mpgd/game-services` use it,
236
+ * and ledgers written by v1 releases keep deduplicating against it.
237
+ */
228
238
  export async function createGooglePlayTokenPurchaseKey(purchaseToken) {
229
239
  if (readIdentifier(purchaseToken, 4096) === undefined) {
230
240
  throw new TypeError('Google Play purchase token is invalid.');
@@ -3307,6 +3317,207 @@ export const isCommerceCapabilities = (() => {
3307
3317
  }));
3308
3318
  return (input, _exceptionable = true) => "object" === typeof input && null !== input && _io0(input, true);
3309
3319
  })();
3320
+ export const assertCommerceServiceCapabilities = (() => {
3321
+ const _ao0 = (input, _path, _exceptionable = true) => ("string" === typeof input.commerceProtocolVersion && (1 <= input.commerceProtocolVersion.length || _assertGuard_1._assertGuard(_exceptionable, {
3322
+ method: "typia.createAssertEquals",
3323
+ path: _path + ".commerceProtocolVersion",
3324
+ expected: "string & CommerceMinLength<1>",
3325
+ value: input.commerceProtocolVersion
3326
+ }, _errorFactory)) || _assertGuard_1._assertGuard(_exceptionable, {
3327
+ method: "typia.createAssertEquals",
3328
+ path: _path + ".commerceProtocolVersion",
3329
+ expected: "(string & CommerceMinLength<1>)",
3330
+ value: input.commerceProtocolVersion
3331
+ }, _errorFactory)) && ((Array.isArray(input.stores) || _assertGuard_1._assertGuard(_exceptionable, {
3332
+ method: "typia.createAssertEquals",
3333
+ path: _path + ".stores",
3334
+ expected: "ReadonlyArray<CommerceServiceStoreCapabilities>",
3335
+ value: input.stores
3336
+ }, _errorFactory)) && input.stores.every((elem, _index3) => ("object" === typeof elem && null !== elem || _assertGuard_1._assertGuard(_exceptionable, {
3337
+ method: "typia.createAssertEquals",
3338
+ path: _path + ".stores[" + _index3 + "]",
3339
+ expected: "CommerceServiceStoreCapabilities",
3340
+ value: elem
3341
+ }, _errorFactory)) && _ao1(elem, _path + ".stores[" + _index3 + "]", true && _exceptionable) || _assertGuard_1._assertGuard(_exceptionable, {
3342
+ method: "typia.createAssertEquals",
3343
+ path: _path + ".stores[" + _index3 + "]",
3344
+ expected: "CommerceServiceStoreCapabilities",
3345
+ value: elem
3346
+ }, _errorFactory)) || _assertGuard_1._assertGuard(_exceptionable, {
3347
+ method: "typia.createAssertEquals",
3348
+ path: _path + ".stores",
3349
+ expected: "ReadonlyArray<CommerceServiceStoreCapabilities>",
3350
+ value: input.stores
3351
+ }, _errorFactory)) && (2 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
3352
+ if (["commerceProtocolVersion", "stores"].some(prop => key === prop))
3353
+ return true;
3354
+ const value = input[key];
3355
+ if (undefined === value)
3356
+ return true;
3357
+ return _assertGuard_1._assertGuard(_exceptionable, {
3358
+ method: "typia.createAssertEquals",
3359
+ path: _path + _accessExpressionAsString_1._accessExpressionAsString(key),
3360
+ expected: "undefined",
3361
+ value: value
3362
+ }, _errorFactory);
3363
+ })));
3364
+ const _ao1 = (input, _path, _exceptionable = true) => ("string" === typeof input.store && (1 <= input.store.length || _assertGuard_1._assertGuard(_exceptionable, {
3365
+ method: "typia.createAssertEquals",
3366
+ path: _path + ".store",
3367
+ expected: "string & CommerceMinLength<1>",
3368
+ value: input.store
3369
+ }, _errorFactory)) || _assertGuard_1._assertGuard(_exceptionable, {
3370
+ method: "typia.createAssertEquals",
3371
+ path: _path + ".store",
3372
+ expected: "(string & CommerceMinLength<1>)",
3373
+ value: input.store
3374
+ }, _errorFactory)) && ((Array.isArray(input.schemas) || _assertGuard_1._assertGuard(_exceptionable, {
3375
+ method: "typia.createAssertEquals",
3376
+ path: _path + ".schemas",
3377
+ expected: "ReadonlyArray<NonEmptyString>",
3378
+ value: input.schemas
3379
+ }, _errorFactory)) && input.schemas.every((elem, _index4) => "string" === typeof elem && (1 <= elem.length || _assertGuard_1._assertGuard(_exceptionable, {
3380
+ method: "typia.createAssertEquals",
3381
+ path: _path + ".schemas[" + _index4 + "]",
3382
+ expected: "string & CommerceMinLength<1>",
3383
+ value: elem
3384
+ }, _errorFactory)) || _assertGuard_1._assertGuard(_exceptionable, {
3385
+ method: "typia.createAssertEquals",
3386
+ path: _path + ".schemas[" + _index4 + "]",
3387
+ expected: "(string & CommerceMinLength<1>)",
3388
+ value: elem
3389
+ }, _errorFactory)) || _assertGuard_1._assertGuard(_exceptionable, {
3390
+ method: "typia.createAssertEquals",
3391
+ path: _path + ".schemas",
3392
+ expected: "ReadonlyArray<NonEmptyString>",
3393
+ value: input.schemas
3394
+ }, _errorFactory)) && ("client" === input.finish || "host" === input.finish || "none" === input.finish || "server" === input.finish || _assertGuard_1._assertGuard(_exceptionable, {
3395
+ method: "typia.createAssertEquals",
3396
+ path: _path + ".finish",
3397
+ expected: "(\"client\" | \"host\" | \"none\" | \"server\")",
3398
+ value: input.finish
3399
+ }, _errorFactory)) && (("object" === typeof input.purchase && null !== input.purchase || _assertGuard_1._assertGuard(_exceptionable, {
3400
+ method: "typia.createAssertEquals",
3401
+ path: _path + ".purchase",
3402
+ expected: "{ readonly consumable: boolean; readonly durable: boolean; readonly subscription: boolean; }",
3403
+ value: input.purchase
3404
+ }, _errorFactory)) && _ao2(input.purchase, _path + ".purchase", true && _exceptionable) || _assertGuard_1._assertGuard(_exceptionable, {
3405
+ method: "typia.createAssertEquals",
3406
+ path: _path + ".purchase",
3407
+ expected: "{ readonly consumable: boolean; readonly durable: boolean; readonly subscription: boolean; }",
3408
+ value: input.purchase
3409
+ }, _errorFactory)) && (4 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
3410
+ if (["store", "schemas", "finish", "purchase"].some(prop => key === prop))
3411
+ return true;
3412
+ const value = input[key];
3413
+ if (undefined === value)
3414
+ return true;
3415
+ return _assertGuard_1._assertGuard(_exceptionable, {
3416
+ method: "typia.createAssertEquals",
3417
+ path: _path + _accessExpressionAsString_1._accessExpressionAsString(key),
3418
+ expected: "undefined",
3419
+ value: value
3420
+ }, _errorFactory);
3421
+ })));
3422
+ const _ao2 = (input, _path, _exceptionable = true) => ("boolean" === typeof input.consumable || _assertGuard_1._assertGuard(_exceptionable, {
3423
+ method: "typia.createAssertEquals",
3424
+ path: _path + ".consumable",
3425
+ expected: "boolean",
3426
+ value: input.consumable
3427
+ }, _errorFactory)) && ("boolean" === typeof input.durable || _assertGuard_1._assertGuard(_exceptionable, {
3428
+ method: "typia.createAssertEquals",
3429
+ path: _path + ".durable",
3430
+ expected: "boolean",
3431
+ value: input.durable
3432
+ }, _errorFactory)) && ("boolean" === typeof input.subscription || _assertGuard_1._assertGuard(_exceptionable, {
3433
+ method: "typia.createAssertEquals",
3434
+ path: _path + ".subscription",
3435
+ expected: "boolean",
3436
+ value: input.subscription
3437
+ }, _errorFactory)) && (3 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).every(key => {
3438
+ if (["consumable", "durable", "subscription"].some(prop => key === prop))
3439
+ return true;
3440
+ const value = input[key];
3441
+ if (undefined === value)
3442
+ return true;
3443
+ return _assertGuard_1._assertGuard(_exceptionable, {
3444
+ method: "typia.createAssertEquals",
3445
+ path: _path + _accessExpressionAsString_1._accessExpressionAsString(key),
3446
+ expected: "undefined",
3447
+ value: value
3448
+ }, _errorFactory);
3449
+ })));
3450
+ const _io0 = (input, _exceptionable = true) => "string" === typeof input.commerceProtocolVersion && 1 <= input.commerceProtocolVersion.length && (Array.isArray(input.stores) && input.stores.every((elem, _index1) => "object" === typeof elem && null !== elem && _io1(elem, true && _exceptionable))) && (2 === Object.keys(input).length || Object.keys(input).every(key => {
3451
+ if (["commerceProtocolVersion", "stores"].some(prop => key === prop))
3452
+ return true;
3453
+ const value = input[key];
3454
+ if (undefined === value)
3455
+ return true;
3456
+ return false;
3457
+ }));
3458
+ const _io1 = (input, _exceptionable = true) => "string" === typeof input.store && 1 <= input.store.length && (Array.isArray(input.schemas) && input.schemas.every((elem, _index2) => "string" === typeof elem && 1 <= elem.length)) && ("client" === input.finish || "host" === input.finish || "none" === input.finish || "server" === input.finish) && ("object" === typeof input.purchase && null !== input.purchase && _io2(input.purchase, true && _exceptionable)) && (4 === Object.keys(input).length || Object.keys(input).every(key => {
3459
+ if (["store", "schemas", "finish", "purchase"].some(prop => key === prop))
3460
+ return true;
3461
+ const value = input[key];
3462
+ if (undefined === value)
3463
+ return true;
3464
+ return false;
3465
+ }));
3466
+ const _io2 = (input, _exceptionable = true) => "boolean" === typeof input.consumable && "boolean" === typeof input.durable && "boolean" === typeof input.subscription && (3 === Object.keys(input).length || Object.keys(input).every(key => {
3467
+ if (["consumable", "durable", "subscription"].some(prop => key === prop))
3468
+ return true;
3469
+ const value = input[key];
3470
+ if (undefined === value)
3471
+ return true;
3472
+ return false;
3473
+ }));
3474
+ const __is = (input, _exceptionable = true) => "object" === typeof input && null !== input && _io0(input, true);
3475
+ let _errorFactory;
3476
+ return (input, errorFactory) => {
3477
+ if (false === __is(input)) {
3478
+ _errorFactory = errorFactory;
3479
+ ((input, _path, _exceptionable = true) => ("object" === typeof input && null !== input || _assertGuard_1._assertGuard(true, {
3480
+ method: "typia.createAssertEquals",
3481
+ path: _path + "",
3482
+ expected: "CommerceServiceCapabilities",
3483
+ value: input
3484
+ }, _errorFactory)) && _ao0(input, _path + "", true) || _assertGuard_1._assertGuard(true, {
3485
+ method: "typia.createAssertEquals",
3486
+ path: _path + "",
3487
+ expected: "CommerceServiceCapabilities",
3488
+ value: input
3489
+ }, _errorFactory))(input, "$input", true);
3490
+ }
3491
+ return input;
3492
+ };
3493
+ })();
3494
+ export const isCommerceServiceCapabilities = (() => {
3495
+ const _io0 = (input, _exceptionable = true) => "string" === typeof input.commerceProtocolVersion && 1 <= input.commerceProtocolVersion.length && (Array.isArray(input.stores) && input.stores.every((elem, _index1) => "object" === typeof elem && null !== elem && _io1(elem, true && _exceptionable))) && (2 === Object.keys(input).length || Object.keys(input).every(key => {
3496
+ if (["commerceProtocolVersion", "stores"].some(prop => key === prop))
3497
+ return true;
3498
+ const value = input[key];
3499
+ if (undefined === value)
3500
+ return true;
3501
+ return false;
3502
+ }));
3503
+ const _io1 = (input, _exceptionable = true) => "string" === typeof input.store && 1 <= input.store.length && (Array.isArray(input.schemas) && input.schemas.every((elem, _index2) => "string" === typeof elem && 1 <= elem.length)) && ("client" === input.finish || "host" === input.finish || "none" === input.finish || "server" === input.finish) && ("object" === typeof input.purchase && null !== input.purchase && _io2(input.purchase, true && _exceptionable)) && (4 === Object.keys(input).length || Object.keys(input).every(key => {
3504
+ if (["store", "schemas", "finish", "purchase"].some(prop => key === prop))
3505
+ return true;
3506
+ const value = input[key];
3507
+ if (undefined === value)
3508
+ return true;
3509
+ return false;
3510
+ }));
3511
+ const _io2 = (input, _exceptionable = true) => "boolean" === typeof input.consumable && "boolean" === typeof input.durable && "boolean" === typeof input.subscription && (3 === Object.keys(input).length || Object.keys(input).every(key => {
3512
+ if (["consumable", "durable", "subscription"].some(prop => key === prop))
3513
+ return true;
3514
+ const value = input[key];
3515
+ if (undefined === value)
3516
+ return true;
3517
+ return false;
3518
+ }));
3519
+ return (input, _exceptionable = true) => "object" === typeof input && null !== input && _io0(input, true);
3520
+ })();
3310
3521
  export const assertCommerceErrorInfo = (() => {
3311
3522
  const _iv0 = new Set(["ACCOUNT_BINDING_REQUIRED", "ACTION_REQUIRED", "ALREADY_OWNED", "CONFIGURATION_REQUIRED", "INSUFFICIENT_BALANCE", "INVALID_RESPONSE", "IN_PROGRESS", "ITEM_UNAVAILABLE", "NETWORK", "NOT_PREPARED", "OUTCOME_UNKNOWN", "STORE_UNAVAILABLE", "UNKNOWN", "USER_CANCELLED"]);
3312
3523
  const _av0 = new Set(["ACCOUNT_BINDING_REQUIRED", "ACTION_REQUIRED", "ALREADY_OWNED", "CONFIGURATION_REQUIRED", "INSUFFICIENT_BALANCE", "INVALID_RESPONSE", "IN_PROGRESS", "ITEM_UNAVAILABLE", "NETWORK", "NOT_PREPARED", "OUTCOME_UNKNOWN", "STORE_UNAVAILABLE", "UNKNOWN", "USER_CANCELLED"]);
@@ -5554,95 +5765,3 @@ export const assertCommerceBridgeRestorePurchasesResult = (() => {
5554
5765
  return input;
5555
5766
  };
5556
5767
  })();
5557
- const purchaseKindToProductType = Object.freeze({
5558
- consumable: 'consumable',
5559
- durable: 'non_consumable',
5560
- subscription: 'subscription',
5561
- });
5562
- /**
5563
- * Serves v1 `CommerceAdapter` callers from a v2 provider. The wrapper never
5564
- * grants: a `purchased` outcome becomes a `completed` result with no
5565
- * entitlements, and `authoritativeGrant` appears only when the provider reports
5566
- * an adapter settlement that already points at a ledger entry. `restore`
5567
- * returns settled purchases from `getAvailablePurchases` and no entitlements;
5568
- * entitlements come from the ledger, not from the provider.
5569
- */
5570
- export function toCommerceAdapter(provider, options = {}) {
5571
- const includePending = options.includePending ?? false;
5572
- return {
5573
- async getProducts() {
5574
- const products = await provider.fetchProducts({ type: 'all' });
5575
- return products.map((product) => ({
5576
- id: product.logicalId,
5577
- type: purchaseKindToProductType[product.kind],
5578
- title: product.title,
5579
- description: product.description,
5580
- price: { formatted: product.price.formatted, currencyCode: product.price.currencyCode },
5581
- }));
5582
- },
5583
- async purchase(input) {
5584
- const result = await provider.requestPurchase({
5585
- logicalId: input.productId,
5586
- idempotencyKey: input.idempotencyKey,
5587
- source: input.source,
5588
- });
5589
- return toPurchaseResult(result);
5590
- },
5591
- async restore() {
5592
- await provider.restorePurchases?.();
5593
- const purchases = await provider.getAvailablePurchases({ includePending });
5594
- const settledPurchases = [];
5595
- for (const purchase of purchases) {
5596
- // A pending purchase is never a settled outcome, even when the adapter
5597
- // already attached a ledger pointer to it.
5598
- if (purchase.settlement === undefined
5599
- || purchase.logicalId === undefined
5600
- || purchase.state === 'pending') {
5601
- continue;
5602
- }
5603
- settledPurchases.push({
5604
- transactionId: purchase.transactionId,
5605
- productId: purchase.logicalId,
5606
- status: purchase.state === 'revoked' ? 'refunded' : 'granted',
5607
- ledgerEntryId: purchase.settlement.ledgerEntryId,
5608
- });
5609
- }
5610
- return { restoredEntitlements: [], settledPurchases };
5611
- },
5612
- async getEntitlements() {
5613
- return [];
5614
- },
5615
- };
5616
- }
5617
- function toPurchaseResult(result) {
5618
- if (result.outcome === 'launched') {
5619
- return { status: 'pending', entitlementIds: [] };
5620
- }
5621
- if (result.outcome === 'purchased') {
5622
- const { purchase } = result;
5623
- return {
5624
- status: purchase.state === 'purchased' ? 'completed' : 'pending',
5625
- transactionId: purchase.transactionId,
5626
- entitlementIds: [],
5627
- evidence: purchase.evidence,
5628
- ...(purchase.settlement === undefined || purchase.state !== 'purchased'
5629
- ? {}
5630
- : { authoritativeGrant: { ledgerEntryId: purchase.settlement.ledgerEntryId } }),
5631
- };
5632
- }
5633
- return {
5634
- status: result.outcome,
5635
- entitlementIds: [],
5636
- diagnostic: toCommerceDiagnostic(result.error),
5637
- };
5638
- }
5639
- function toCommerceDiagnostic(error) {
5640
- const providerCode = error.storeCode;
5641
- return {
5642
- code: error.code,
5643
- retryable: error.retryable,
5644
- ...(providerCode !== undefined && /^[A-Za-z0-9_.:-]{1,128}$/u.test(providerCode)
5645
- ? { providerCode }
5646
- : {}),
5647
- };
5648
- }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { AdProvider } from './ads.js';
2
2
  import type { CommerceProvider } from './commerce-contract.js';
3
+ import { createUnsupportedCommerceCapabilities } from './commerce-capabilities.js';
4
+ import type { NotificationCapabilities, NotificationProvider } from './notifications-contract.js';
5
+ export { createUnsupportedCommerceCapabilities };
3
6
  export type PlatformTarget = 'browser' | 'microsoft-store' | 'android' | 'ios' | 'ait' | 'reddit' | 'verse8' | 'crazygames' | 'oneplay' | 'telegram' | 'tauri' | 'wechat' | 'tiktok';
4
7
  export type StarterLogicalProductId = 'COINS_100' | 'COINS_500' | 'REMOVE_ADS';
5
8
  export type LogicalProductId = StarterLogicalProductId | (string & Record<never, never>);
@@ -57,12 +60,23 @@ export interface CommerceDiagnostic extends PlatformOperationFailure {
57
60
  /** Provider SDK error code, passed through only when it is a safe identifier. */
58
61
  readonly providerCode?: string;
59
62
  }
63
+ /**
64
+ * Purchase result vocabulary of the adapter engines and of
65
+ * `GameServicesPurchaseResult.purchase`. It is no gateway surface: a gateway
66
+ * sells through `PlatformGateway.commerce`, and nothing here is ever an input
67
+ * to a grant.
68
+ */
60
69
  export interface PurchaseResult {
61
70
  readonly status: 'completed' | 'cancelled' | 'pending' | 'failed';
62
71
  readonly transactionId?: string;
63
72
  readonly entitlementIds: readonly string[];
64
73
  readonly evidence?: PlatformEvidenceEnvelope;
65
- /** Present only when the adapter already completed the authoritative server grant. */
74
+ /**
75
+ * An adapter engine's record that its own authority already wrote a ledger
76
+ * entry. Informational only: no client code reads it to grant, and a
77
+ * provider's `settlement` pointer is verified through the backend by the
78
+ * reconciler like any other evidence.
79
+ */
66
80
  readonly authoritativeGrant?: Readonly<{
67
81
  readonly ledgerEntryId: string;
68
82
  readonly alreadyProcessed?: boolean;
@@ -86,19 +100,6 @@ export interface PurchaseSettlement {
86
100
  /** Client purchase idempotency key the order was started with, when the adapter can link it. */
87
101
  readonly idempotencyKey?: string;
88
102
  }
89
- /**
90
- * Resolve a specific purchase from authoritative checkout or restore evidence.
91
- * A completed native checkout without authoritativeGrant is still unconfirmed.
92
- * This is UI status evidence, never permission to credit a wallet locally.
93
- * The caller must pair `purchase` with its requested `productId` because
94
- * PurchaseResult itself does not carry a product identifier.
95
- */
96
- export declare function findAuthoritativePurchaseSettlement(input: {
97
- readonly productId: LogicalProductId;
98
- readonly transactionId?: string;
99
- readonly purchase?: PurchaseResult;
100
- readonly restore?: PurchaseRestoreResult;
101
- }): PurchaseSettlement | null;
102
103
  export interface RewardedAdResult {
103
104
  readonly status: 'completed' | 'pending' | 'skipped' | 'unavailable' | 'failed';
104
105
  readonly rewardGranted: boolean;
@@ -123,20 +124,6 @@ export interface PlatformPurchasePresentationEvent {
123
124
  readonly sequence: number;
124
125
  readonly state: 'open' | 'closed' | 'not-started' | 'unknown';
125
126
  }
126
- export interface CommerceAdapter {
127
- /** Trusted native presentation facts, independent of the purchase grant result. */
128
- readonly presentation?: {
129
- subscribe(listener: (event: PlatformPurchasePresentationEvent) => void): () => void;
130
- };
131
- getProducts(): Promise<readonly ProductInfo[]>;
132
- purchase(input: {
133
- readonly productId: LogicalProductId;
134
- readonly source: 'shop' | 'stage_fail' | 'result' | 'event';
135
- readonly idempotencyKey: string;
136
- }): Promise<PurchaseResult>;
137
- restore?(): Promise<PurchaseRestoreResult>;
138
- getEntitlements(): Promise<readonly Entitlement[]>;
139
- }
140
127
  export interface AdAdapter {
141
128
  /** Optional versioned provider surface; v1 methods remain compatible. */
142
129
  readonly provider?: AdProvider;
@@ -208,7 +195,7 @@ export interface LeaderboardAdapter {
208
195
  /** Readiness of an installed platform provider, independent of target policy. */
209
196
  export type PlatformProviderAvailability = 'unsupported' | 'configuration-required' | 'action-required' | 'temporarily-unavailable' | 'available';
210
197
  /** Existing capabilities and integrations annotated by optional providers. */
211
- export type PlatformProviderFeature = 'nativeIap' | 'subscriptionIap' | 'rewardedAds' | 'interstitialAds' | 'bannerAds' | 'nativeLeaderboard' | 'identityUpgrade' | 'pushNotifications';
198
+ export type PlatformProviderFeature = 'rewardedAds' | 'interstitialAds' | 'bannerAds' | 'nativeLeaderboard' | 'identityUpgrade' | 'pushNotifications';
212
199
  export type CommerceFinishOwner = 'client' | 'server' | 'host' | 'none';
213
200
  export type CommerceEvidenceAuthority = 'client-token' | 'host-callback' | 'adapter-settled' | 'server-push';
214
201
  export type CommerceAccountBindingPolicy = 'required' | 'optional' | 'host';
@@ -249,9 +236,6 @@ export interface CommerceCapabilities {
249
236
  readonly availability: PlatformProviderAvailability;
250
237
  }
251
238
  export interface PlatformCapabilities {
252
- readonly nativeIap: boolean;
253
- /** Optional for adapters published before subscription-specific support. */
254
- readonly subscriptionIap?: boolean;
255
239
  readonly nativeAds: boolean;
256
240
  readonly rewardedAds: boolean;
257
241
  readonly interstitialAds: boolean;
@@ -268,11 +252,23 @@ export interface PlatformCapabilities {
268
252
  /** Explains a false capability without claiming an unavailable SDK works. */
269
253
  readonly providerAvailability?: Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>;
270
254
  /**
271
- * Optional commerce v2 capability block. When present it must be a fresh
272
- * object on every read with no unknown keys, and `nativeIap` and
273
- * `subscriptionIap` must equal `deriveLegacyCommerceCapabilities(commerce)`.
255
+ * The one source of commerce capabilities: what the gateway's commerce
256
+ * provider sells, who finishes a purchase, and what evidence authorizes a
257
+ * grant. A gateway without a commerce provider reports
258
+ * `createUnsupportedCommerceCapabilities()`. The block must be a fresh object
259
+ * on every read with no unknown keys.
274
260
  */
275
- readonly commerce?: CommerceCapabilities;
261
+ readonly commerce: CommerceCapabilities;
262
+ /**
263
+ * Optional notifications v2 capability block of the `notificationsV2`
264
+ * provider the gateway exposes: present exactly when the gateway exposes
265
+ * one, raw adapters included. `withTargetAvailability` in
266
+ * `@mpgd/target-config` adds it from that provider and leaves it out while
267
+ * the provider is hidden. When present it must be a fresh object on every
268
+ * read with no unknown keys, and `providerAvailability.pushNotifications`
269
+ * must equal its `availability`.
270
+ */
271
+ readonly notifications?: NotificationCapabilities;
276
272
  }
277
273
  export interface PlayerIdentity {
278
274
  readonly playerId: string;
@@ -317,6 +313,18 @@ export interface ShareIntent {
317
313
  readonly kind: 'daily-result' | 'friend-challenge' | 'invite';
318
314
  readonly title: string;
319
315
  readonly text: string;
316
+ /**
317
+ * A root-relative link on the game's origin (`/daily?challengeToken=…`) or an
318
+ * absolute `https://` (or `http://`) link. Every adapter whose host builds or
319
+ * receives a share URL checks it with `normalizeShareLink` from
320
+ * `@mpgd/platform/routes`, most through that module's `withShareIntentCheck`,
321
+ * hands the host the normalized link, and answers `unavailable` when the rule
322
+ * refuses it: a link that could name another host, contains a control
323
+ * character, or has another form, such as a page-relative link. Apps in Toss
324
+ * also accepts its own `intoss://<app>` links. An adapter whose host shares
325
+ * no link ignores it: WeChat's card reopens the game with `query` built from
326
+ * `kind` and `payload`.
327
+ */
320
328
  readonly deepLink: string;
321
329
  readonly payload?: SharePayload;
322
330
  readonly previewImageUrl?: string;
@@ -357,6 +365,18 @@ export interface NotificationSubscriptionAdapter {
357
365
  export interface NativeOpenUrlEvent {
358
366
  readonly url: string;
359
367
  readonly source: 'cold' | 'warm';
368
+ /**
369
+ * For a game link, the URL's path, query, and fragment, normalized with
370
+ * `normalizeRootRelativeRoute` from `@mpgd/platform/routes`. The host is not
371
+ * part of it: `mygame://game/daily` and `https://game.example/daily` give
372
+ * `/daily`, and `mygame://level/3` gives `/3`. Adapters do not deliver a game
373
+ * link whose path that rule refuses, so a router that routes by path opens
374
+ * this route as is. It is absent when the URL has no path to check, such as
375
+ * `mygame:daily` or `mygame://daily`. A router that builds its route from
376
+ * anything else in the URL, such as a custom-scheme host, checks that route
377
+ * with `normalizeRootRelativeRoute` and opens only what it returns.
378
+ */
379
+ readonly route?: string;
360
380
  }
361
381
  export interface NativeBackButtonEvent {
362
382
  /** Whether the WebView history can navigate back, not the game's own scene stack. */
@@ -413,7 +433,11 @@ export interface LifecycleAdapter {
413
433
  * are not guaranteed to run before process termination.
414
434
  */
415
435
  onBackButton?(handler: (event: NativeBackButtonEvent) => boolean | Promise<boolean>): () => void;
416
- /** Game links and OAuth redirects use separate callbacks. */
436
+ /**
437
+ * Game links and OAuth redirects use separate callbacks. A game link is
438
+ * untrusted input: open its checked `route`, or check any route built from
439
+ * `url` with `normalizeRootRelativeRoute` (see `NativeOpenUrlEvent.route`).
440
+ */
417
441
  onGameUrlOpen?(callback: (event: NativeOpenUrlEvent) => void): () => void;
418
442
  onOAuthRedirect?(callback: (event: NativeOpenUrlEvent) => void): () => void;
419
443
  getInitialGameUrl?(): Promise<NativeOpenUrlEvent | null>;
@@ -481,14 +505,14 @@ export interface PlatformGateway {
481
505
  readonly gameLoading?: GameLoadingAdapter;
482
506
  getCapabilities(): Promise<PlatformCapabilities>;
483
507
  readonly identity: IdentityAdapter;
484
- readonly commerce: CommerceAdapter;
485
508
  /**
486
- * Optional commerce v2 provider (`@mpgd/platform/commerce`). Transitional:
487
- * adapters expose it next to the v1 `commerce` adapter until C4 removes v1,
488
- * and the game-services reconciler (C1) prefers it when present. Absent on
489
- * gateways whose target has no v2 provider.
509
+ * The commerce provider (`@mpgd/platform/commerce`), the only commerce
510
+ * surface of a gateway. Games purchase through `@mpgd/game-services`, whose
511
+ * reconciler drives it; a scene never calls it directly. Absent on gateways
512
+ * whose target sells nothing, which then report
513
+ * `createUnsupportedCommerceCapabilities()`.
490
514
  */
491
- readonly commerceV2?: CommerceProvider;
515
+ readonly commerce?: CommerceProvider;
492
516
  readonly ads: AdAdapter;
493
517
  readonly leaderboard: LeaderboardAdapter;
494
518
  readonly lifecycle: LifecycleAdapter;
@@ -499,6 +523,19 @@ export interface PlatformGateway {
499
523
  readonly presentation?: PresentationAdapter;
500
524
  readonly sharing?: ShareAdapter;
501
525
  readonly notifications?: NotificationSubscriptionAdapter;
526
+ /**
527
+ * Optional notifications v2 provider (`@mpgd/platform/notifications`).
528
+ * Transitional: adapters expose it next to the v1 `notifications` adapter
529
+ * until the major release that removes v1. Games do not call it directly:
530
+ * the game-services client composes it with the backend into the
531
+ * notifications runtime, and `toNotificationSubscriptionAdapter` serves v1
532
+ * callers from that runtime with the topics resolved for the target. Absent
533
+ * on gateways whose target has no v2 provider. `withTargetAvailability` in
534
+ * `@mpgd/target-config` exposes it only while `integrations.notifications`
535
+ * lets the game call notifications, as it does the v1 adapter, and then
536
+ * adds `PlatformCapabilities.notifications` from it.
537
+ */
538
+ readonly notificationsV2?: NotificationProvider;
502
539
  readonly promotions?: PromotionRewardAdapter;
503
540
  readonly gameActivity?: GameActivityAdapter;
504
541
  readonly gameSettings?: GameSettingsAdapter;
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { createUnsupportedCommerceCapabilities } from './commerce-capabilities.js';
2
+ export { createUnsupportedCommerceCapabilities };
1
3
  /**
2
4
  * Provider-neutral error raised when a platform bridge rejects an operation.
3
5
  *
@@ -41,41 +43,6 @@ function isPlatformOperationCode(code) {
41
43
  && code.length <= 128
42
44
  && /^[A-Z][A-Z0-9_:-]*$/u.test(code);
43
45
  }
44
- /**
45
- * Resolve a specific purchase from authoritative checkout or restore evidence.
46
- * A completed native checkout without authoritativeGrant is still unconfirmed.
47
- * This is UI status evidence, never permission to credit a wallet locally.
48
- * The caller must pair `purchase` with its requested `productId` because
49
- * PurchaseResult itself does not carry a product identifier.
50
- */
51
- export function findAuthoritativePurchaseSettlement(input) {
52
- const effectiveTransactionId = input.transactionId ?? input.purchase?.transactionId;
53
- const matchingRestored = input.restore?.settledPurchases?.filter((settlement) => settlement.productId === input.productId
54
- && (effectiveTransactionId === undefined || settlement.transactionId === effectiveTransactionId));
55
- // Without an order ID, multiple historical outcomes are ambiguous.
56
- if (matchingRestored !== undefined && matchingRestored.length > 1) {
57
- return null;
58
- }
59
- if (matchingRestored?.[0] !== undefined) {
60
- return matchingRestored[0];
61
- }
62
- const purchase = input.purchase;
63
- if (purchase?.status !== 'completed'
64
- || purchase.transactionId === undefined
65
- || purchase.authoritativeGrant === undefined
66
- || (effectiveTransactionId !== undefined && effectiveTransactionId !== purchase.transactionId)) {
67
- return null;
68
- }
69
- return {
70
- transactionId: purchase.transactionId,
71
- productId: input.productId,
72
- status: 'granted',
73
- ledgerEntryId: purchase.authoritativeGrant.ledgerEntryId,
74
- ...(purchase.authoritativeGrant.alreadyProcessed === undefined
75
- ? {}
76
- : { alreadyProcessed: purchase.authoritativeGrant.alreadyProcessed }),
77
- };
78
- }
79
46
  export function resolveShareCompletion(result) {
80
47
  if (result.status !== 'shared') {
81
48
  return undefined;
@@ -87,7 +54,6 @@ export function isShareCompleted(result) {
87
54
  }
88
55
  export function createUnsupportedCapabilities() {
89
56
  return {
90
- nativeIap: false,
91
57
  nativeAds: false,
92
58
  rewardedAds: false,
93
59
  interstitialAds: false,
@@ -98,5 +64,6 @@ export function createUnsupportedCapabilities() {
98
64
  socialShare: false,
99
65
  haptics: false,
100
66
  localizedContent: false,
67
+ commerce: createUnsupportedCommerceCapabilities(),
101
68
  };
102
69
  }