@mpgd/platform 0.9.0 → 0.11.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.
@@ -1,5 +1,6 @@
1
1
  import type { PlatformCapabilities, PlatformGateway, PlatformTarget } from './index.js';
2
2
  export declare const platformCapabilityKeys: readonly ["nativeIap", "nativeAds", "rewardedAds", "interstitialAds", "nativeLeaderboard", "remoteLeaderboard", "achievements", "cloudSave", "socialShare", "haptics", "localizedContent"];
3
+ export declare const optionalPlatformCapabilityKeys: readonly ["bannerAds"];
3
4
  export interface PlatformGatewayCapabilityConformanceTransition {
4
5
  readonly update: () => Promise<void> | void;
5
6
  readonly expectedCapabilities: PlatformCapabilities;
@@ -11,6 +11,15 @@ export const platformCapabilityKeys = Object.freeze([
11
11
  'haptics',
12
12
  'localizedContent',
13
13
  ]);
14
+ export const optionalPlatformCapabilityKeys = Object.freeze([
15
+ 'bannerAds',
16
+ ]);
17
+ const allowedPlatformCapabilityKeys = Object.freeze([
18
+ ...platformCapabilityKeys,
19
+ ...optionalPlatformCapabilityKeys,
20
+ ]);
21
+ const allowedPlatformCapabilityKeySet = new Set(allowedPlatformCapabilityKeys);
22
+ const optionalPlatformCapabilityKeySet = new Set(optionalPlatformCapabilityKeys);
14
23
  /**
15
24
  * Runs provider-neutral capability checks against real adapter gateways or
16
25
  * target-configured wrappers. Every read must return a complete boolean
@@ -61,11 +70,15 @@ async function runFixture(fixture) {
61
70
  }
62
71
  function assertCapabilitySnapshot(actual, expected) {
63
72
  const actualKeys = Object.keys(actual).sort();
64
- const expectedKeys = [...platformCapabilityKeys].sort();
65
- assertEqual(JSON.stringify(actualKeys), JSON.stringify(expectedKeys), 'capability snapshots must contain exactly the public capability keys');
66
- for (const key of platformCapabilityKeys) {
67
- assertEqual(typeof actual[key], 'boolean', `capability ${key} must be a boolean`);
68
- assertEqual(actual[key], expected[key], `capability ${key} must match the expected provider state`);
73
+ const missingKeys = platformCapabilityKeys.filter((key) => !Object.hasOwn(actual, key));
74
+ const unexpectedKeys = actualKeys.filter((key) => !allowedPlatformCapabilityKeySet.has(key));
75
+ assertEqual(JSON.stringify({ missingKeys, unexpectedKeys }), JSON.stringify({ missingKeys: [], unexpectedKeys: [] }), 'capability snapshots must contain all required keys and no unknown keys');
76
+ for (const key of allowedPlatformCapabilityKeys) {
77
+ const optional = optionalPlatformCapabilityKeySet.has(key);
78
+ if (!optional || Object.hasOwn(actual, key)) {
79
+ assertEqual(typeof actual[key], 'boolean', `capability ${key} must be a boolean`);
80
+ }
81
+ assertEqual(optional ? (actual[key] ?? false) : actual[key], optional ? (expected[key] ?? false) : expected[key], `capability ${key} must match the expected provider state`);
69
82
  }
70
83
  }
71
84
  function tryMutateSnapshot(snapshot) {
@@ -77,7 +90,8 @@ function tryMutateSnapshot(snapshot) {
77
90
  }
78
91
  }
79
92
  function hasCapabilityDifference(first, second) {
80
- return platformCapabilityKeys.some((key) => first[key] !== second[key]);
93
+ return allowedPlatformCapabilityKeys
94
+ .some((key) => (first[key] ?? false) !== (second[key] ?? false));
81
95
  }
82
96
  function assert(condition, message) {
83
97
  if (!condition) {
package/dist/index.d.ts CHANGED
@@ -24,6 +24,28 @@ export interface PlatformEvidenceEnvelope {
24
24
  readonly schema: string;
25
25
  readonly payload: Readonly<Record<string, string | number | boolean>>;
26
26
  }
27
+ export interface PlatformOperationFailure {
28
+ readonly code: string;
29
+ readonly retryable: boolean;
30
+ }
31
+ /**
32
+ * Provider-neutral error raised when a platform bridge rejects an operation.
33
+ *
34
+ * Adapters preserve a stable, non-sensitive code and retry hint so game UI can
35
+ * distinguish configuration, environment, and transient failures without
36
+ * importing a provider SDK or parsing localized error messages.
37
+ */
38
+ export declare class PlatformOperationError extends Error implements PlatformOperationFailure {
39
+ readonly name = "PlatformOperationError";
40
+ readonly code: string;
41
+ readonly retryable: boolean;
42
+ constructor(input?: Readonly<{
43
+ readonly code?: unknown;
44
+ readonly message?: unknown;
45
+ readonly retryable?: unknown;
46
+ }>);
47
+ }
48
+ export declare function readPlatformOperationFailure(error: unknown): PlatformOperationFailure | null;
27
49
  export interface PurchaseResult {
28
50
  readonly status: 'completed' | 'cancelled' | 'pending' | 'failed';
29
51
  readonly transactionId?: string;
@@ -47,6 +69,16 @@ export interface RewardedAdResult {
47
69
  export interface InterstitialAdResult {
48
70
  readonly status: 'shown' | 'skipped' | 'unavailable';
49
71
  }
72
+ /**
73
+ * Result of mounting a platform-rendered inline banner into a game-owned surface.
74
+ * The platform host owns the native/SDK attachment; games only reserve and name
75
+ * a surface, so this contract does not leak DOM or provider types. Adapters must
76
+ * release any partial attachment before resolving `unavailable` or `failed`;
77
+ * games only need to unmount a successfully mounted banner.
78
+ */
79
+ export interface BannerAdMountResult {
80
+ readonly status: 'mounted' | 'unavailable' | 'failed';
81
+ }
50
82
  export interface CommerceAdapter {
51
83
  getProducts(): Promise<readonly ProductInfo[]>;
52
84
  purchase(input: {
@@ -68,6 +100,14 @@ export interface AdAdapter {
68
100
  showInterstitial?(input: {
69
101
  readonly placementId: LogicalAdPlacementId;
70
102
  }): Promise<InterstitialAdResult>;
103
+ mountBanner?(input: {
104
+ readonly placementId: LogicalAdPlacementId;
105
+ /** Host-resolvable identifier for an empty, game-owned inline ad surface. */
106
+ readonly surfaceId: string;
107
+ }): Promise<BannerAdMountResult>;
108
+ unmountBanner?(input: {
109
+ readonly surfaceId: string;
110
+ }): Promise<void>;
71
111
  }
72
112
  export type PromotionRewardAvailability = 'available' | 'configuration-required' | 'unsupported';
73
113
  export type PromotionRewardResult = {
@@ -115,6 +155,8 @@ export interface PlatformCapabilities {
115
155
  readonly nativeAds: boolean;
116
156
  readonly rewardedAds: boolean;
117
157
  readonly interstitialAds: boolean;
158
+ /** Optional for compatibility with adapters published before inline ads. */
159
+ readonly bannerAds?: boolean;
118
160
  readonly nativeLeaderboard: boolean;
119
161
  /** A game-owned server leaderboard is available without a native platform surface. */
120
162
  readonly remoteLeaderboard: boolean;
package/dist/index.js CHANGED
@@ -1,3 +1,46 @@
1
+ /**
2
+ * Provider-neutral error raised when a platform bridge rejects an operation.
3
+ *
4
+ * Adapters preserve a stable, non-sensitive code and retry hint so game UI can
5
+ * distinguish configuration, environment, and transient failures without
6
+ * importing a provider SDK or parsing localized error messages.
7
+ */
8
+ export class PlatformOperationError extends Error {
9
+ name = 'PlatformOperationError';
10
+ code;
11
+ retryable;
12
+ constructor(input) {
13
+ const diagnostics = input ?? {};
14
+ super(typeof diagnostics.message === 'string'
15
+ ? diagnostics.message
16
+ : 'The platform operation failed.');
17
+ this.code = normalizePlatformOperationCode(diagnostics.code);
18
+ this.retryable = diagnostics.retryable === true;
19
+ }
20
+ }
21
+ export function readPlatformOperationFailure(error) {
22
+ if (error instanceof PlatformOperationError) {
23
+ return { code: error.code, retryable: error.retryable };
24
+ }
25
+ if (typeof error !== 'object' || error === null || Array.isArray(error)) {
26
+ return null;
27
+ }
28
+ const record = error;
29
+ return typeof record.code === 'string'
30
+ && isPlatformOperationCode(record.code)
31
+ && typeof record.retryable === 'boolean'
32
+ ? { code: record.code, retryable: record.retryable }
33
+ : null;
34
+ }
35
+ function normalizePlatformOperationCode(code) {
36
+ return isPlatformOperationCode(code) ? code : 'PLATFORM_OPERATION_FAILED';
37
+ }
38
+ function isPlatformOperationCode(code) {
39
+ return typeof code === 'string'
40
+ && code.length > 0
41
+ && code.length <= 128
42
+ && /^[A-Z][A-Z0-9_:-]*$/u.test(code);
43
+ }
1
44
  export function resolveShareCompletion(result) {
2
45
  if (result.status !== 'shared') {
3
46
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/platform",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Shared platform gateway, monetization, ads, leaderboard, identity, lifecycle, and storage contracts for mpgd games.",
5
5
  "license": "MIT",
6
6
  "repository": {