@mpgd/platform 0.22.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/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
  }