@mpgd/platform 0.12.2 → 0.14.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,6 +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
+ export declare const optionalPlatformCapabilityKeys: readonly ["bannerAds", "subscriptionIap"];
4
4
  export interface PlatformGatewayCapabilityConformanceTransition {
5
5
  readonly update: () => Promise<void> | void;
6
6
  readonly expectedCapabilities: PlatformCapabilities;
@@ -27,7 +27,7 @@ export interface PlatformGatewayCapabilityConformanceReport {
27
27
  * target-configured wrappers. Every read must return a complete boolean
28
28
  * snapshot, a fresh object, and the provider's latest state.
29
29
  * @evidence docs/specs/platform-capability-snapshots.md#snapshot-shape Checks each gateway read against the required boolean keys and expected provider state.
30
- * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #f83948b Reviewed the required keys, optional banner default, and fresh reread in runFixture.
30
+ * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #a1834db Reviewed required booleans, optional banner/subscription defaults, ordered readiness comparison, and fresh nested rereads.
31
31
  * @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
32
32
  * @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
33
33
  * @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
@@ -13,19 +13,40 @@ export const platformCapabilityKeys = Object.freeze([
13
13
  ]);
14
14
  export const optionalPlatformCapabilityKeys = Object.freeze([
15
15
  'bannerAds',
16
+ 'subscriptionIap',
16
17
  ]);
17
18
  const allowedPlatformCapabilityKeys = Object.freeze([
18
19
  ...platformCapabilityKeys,
19
20
  ...optionalPlatformCapabilityKeys,
20
21
  ]);
21
- const allowedPlatformCapabilityKeySet = new Set(allowedPlatformCapabilityKeys);
22
+ const allowedPlatformCapabilityKeySet = new Set([
23
+ ...allowedPlatformCapabilityKeys,
24
+ 'providerAvailability',
25
+ ]);
22
26
  const optionalPlatformCapabilityKeySet = new Set(optionalPlatformCapabilityKeys);
27
+ const providerFeatureSet = new Set([
28
+ 'nativeIap',
29
+ 'subscriptionIap',
30
+ 'rewardedAds',
31
+ 'interstitialAds',
32
+ 'bannerAds',
33
+ 'nativeLeaderboard',
34
+ 'identityUpgrade',
35
+ 'pushNotifications',
36
+ ]);
37
+ const providerAvailabilitySet = new Set([
38
+ 'unsupported',
39
+ 'configuration-required',
40
+ 'action-required',
41
+ 'temporarily-unavailable',
42
+ 'available',
43
+ ]);
23
44
  /**
24
45
  * Runs provider-neutral capability checks against real adapter gateways or
25
46
  * target-configured wrappers. Every read must return a complete boolean
26
47
  * snapshot, a fresh object, and the provider's latest state.
27
48
  * @evidence docs/specs/platform-capability-snapshots.md#snapshot-shape Checks each gateway read against the required boolean keys and expected provider state.
28
- * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #f83948b Reviewed the required keys, optional banner default, and fresh reread in runFixture.
49
+ * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #a1834db Reviewed required booleans, optional banner/subscription defaults, ordered readiness comparison, and fresh nested rereads.
29
50
  * @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
30
51
  * @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
31
52
  * @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
@@ -60,10 +81,10 @@ export async function runPlatformGatewayCapabilityConformance(input) {
60
81
  async function runFixture(fixture) {
61
82
  // A fixture may accidentally share its expected object with the provider.
62
83
  // Keep the oracle independent before tryMutateSnapshot probes isolation.
63
- const initialExpected = { ...fixture.expectedCapabilities };
84
+ const initialExpected = cloneExpectedCapabilities(fixture.expectedCapabilities);
64
85
  const transitionExpected = fixture.transition === undefined
65
86
  ? undefined
66
- : { ...fixture.transition.expectedCapabilities };
87
+ : cloneExpectedCapabilities(fixture.transition.expectedCapabilities);
67
88
  assertEqual(fixture.gateway.target, fixture.expectedTarget, 'gateway target must match the fixture target');
68
89
  const first = await fixture.gateway.getCapabilities();
69
90
  assertCapabilitySnapshot(first, initialExpected);
@@ -93,6 +114,29 @@ function assertCapabilitySnapshot(actual, expected) {
93
114
  }
94
115
  assertEqual(optional ? (actual[key] ?? false) : actual[key], optional ? (expected[key] ?? false) : expected[key], `capability ${key} must match the expected provider state`);
95
116
  }
117
+ assertProviderAvailabilityShape(actual.providerAvailability);
118
+ assertEqual(normalizedAvailability(actual.providerAvailability), normalizedAvailability(expected.providerAvailability), 'provider availability must match the expected provider state');
119
+ }
120
+ function assertProviderAvailabilityShape(value) {
121
+ if (value === undefined) {
122
+ return;
123
+ }
124
+ assert(typeof value === 'object' && value !== null && !Array.isArray(value), 'provider availability must be a non-array record');
125
+ for (const [feature, state] of Object.entries(value)) {
126
+ assert(providerFeatureSet.has(feature)
127
+ && providerAvailabilitySet.has(state), 'provider availability must contain only known features and readiness states');
128
+ }
129
+ }
130
+ function cloneExpectedCapabilities(value) {
131
+ return {
132
+ ...value,
133
+ ...(value.providerAvailability === undefined
134
+ ? {}
135
+ : { providerAvailability: { ...value.providerAvailability } }),
136
+ };
137
+ }
138
+ function normalizedAvailability(value) {
139
+ return JSON.stringify(Object.entries(value ?? {}).sort(([a], [b]) => a.localeCompare(b)));
96
140
  }
97
141
  function tryMutateSnapshot(snapshot) {
98
142
  try {
@@ -101,10 +145,23 @@ function tryMutateSnapshot(snapshot) {
101
145
  catch {
102
146
  // Frozen snapshots already satisfy the isolation requirement.
103
147
  }
148
+ if (snapshot.providerAvailability !== undefined) {
149
+ try {
150
+ const next = snapshot.providerAvailability.nativeIap === 'available'
151
+ ? 'unsupported'
152
+ : 'available';
153
+ Reflect.set(snapshot.providerAvailability, 'nativeIap', next);
154
+ }
155
+ catch {
156
+ // A frozen nested readiness record already satisfies isolation.
157
+ }
158
+ }
104
159
  }
105
160
  function hasCapabilityDifference(first, second) {
106
161
  return allowedPlatformCapabilityKeys
107
- .some((key) => (first[key] ?? false) !== (second[key] ?? false));
162
+ .some((key) => (first[key] ?? false) !== (second[key] ?? false))
163
+ || normalizedAvailability(first.providerAvailability)
164
+ !== normalizedAvailability(second.providerAvailability);
108
165
  }
109
166
  function assert(condition, message) {
110
167
  if (!condition) {
package/dist/index.d.ts CHANGED
@@ -83,7 +83,7 @@ export declare function findAuthoritativePurchaseSettlement(input: {
83
83
  readonly restore?: PurchaseRestoreResult;
84
84
  }): PurchaseSettlement | null;
85
85
  export interface RewardedAdResult {
86
- readonly status: 'completed' | 'skipped' | 'unavailable' | 'failed';
86
+ readonly status: 'completed' | 'pending' | 'skipped' | 'unavailable' | 'failed';
87
87
  readonly rewardGranted: boolean;
88
88
  readonly ledgerEntryId?: string;
89
89
  readonly evidence?: PlatformEvidenceEnvelope;
@@ -114,6 +114,8 @@ export interface CommerceAdapter {
114
114
  export interface AdAdapter {
115
115
  preload(input: {
116
116
  readonly placementId: LogicalAdPlacementId;
117
+ /** Allows a shared provider preload method to enforce format readiness. */
118
+ readonly format?: 'rewarded' | 'interstitial' | 'banner';
117
119
  }): Promise<void>;
118
120
  showRewarded(input: {
119
121
  readonly placementId: LogicalAdPlacementId;
@@ -161,6 +163,8 @@ export interface LeaderboardScoreInput {
161
163
  readonly score: number;
162
164
  readonly runId: string;
163
165
  readonly submittedAt: string;
166
+ /** Optional target-selected route when native and remote backends coexist. */
167
+ readonly route?: 'native' | 'remote';
164
168
  }
165
169
  export interface LeaderboardSubmitResult {
166
170
  readonly submitted: boolean;
@@ -170,10 +174,17 @@ export interface LeaderboardAdapter {
170
174
  submitScore(input: LeaderboardScoreInput): Promise<LeaderboardSubmitResult>;
171
175
  open(input?: {
172
176
  readonly leaderboardId?: string;
177
+ readonly route?: 'native' | 'remote';
173
178
  }): Promise<void>;
174
179
  }
180
+ /** Readiness of an installed platform provider, independent of target policy. */
181
+ export type PlatformProviderAvailability = 'unsupported' | 'configuration-required' | 'action-required' | 'temporarily-unavailable' | 'available';
182
+ /** Existing capabilities and integrations annotated by optional providers. */
183
+ export type PlatformProviderFeature = 'nativeIap' | 'subscriptionIap' | 'rewardedAds' | 'interstitialAds' | 'bannerAds' | 'nativeLeaderboard' | 'identityUpgrade' | 'pushNotifications';
175
184
  export interface PlatformCapabilities {
176
185
  readonly nativeIap: boolean;
186
+ /** Optional for adapters published before subscription-specific support. */
187
+ readonly subscriptionIap?: boolean;
177
188
  readonly nativeAds: boolean;
178
189
  readonly rewardedAds: boolean;
179
190
  readonly interstitialAds: boolean;
@@ -187,6 +198,8 @@ export interface PlatformCapabilities {
187
198
  readonly socialShare: boolean;
188
199
  readonly haptics: boolean;
189
200
  readonly localizedContent: boolean;
201
+ /** Explains a false capability without claiming an unavailable SDK works. */
202
+ readonly providerAvailability?: Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>;
190
203
  }
191
204
  export interface PlayerIdentity {
192
205
  readonly playerId: string;
@@ -267,9 +280,77 @@ export interface NotificationSubscriptionAdapter {
267
280
  getStatus(topic: NotificationTopic): Promise<NotificationSubscriptionStatus>;
268
281
  requestSubscription(topic: NotificationTopic): Promise<NotificationSubscriptionResult>;
269
282
  }
283
+ /** The source of an app URL, not a claim that the URL is trusted. */
284
+ export interface NativeOpenUrlEvent {
285
+ readonly url: string;
286
+ readonly source: 'cold' | 'warm';
287
+ }
288
+ export interface NativeBackButtonEvent {
289
+ /** Whether the WebView history can navigate back, not the game's own scene stack. */
290
+ readonly canGoBack: boolean;
291
+ }
292
+ /** Full WebView viewport geometry in CSS pixels, before applying game-owned padding. */
293
+ export interface PlatformViewportInsets {
294
+ readonly top: number;
295
+ readonly right: number;
296
+ readonly bottom: number;
297
+ readonly left: number;
298
+ }
299
+ export interface PlatformViewportBounds {
300
+ readonly x: number;
301
+ readonly y: number;
302
+ readonly width: number;
303
+ readonly height: number;
304
+ }
305
+ /** An occupied game surface with stable identity, not an extra inset to sum. */
306
+ export interface PlatformViewportOccupiedSurface {
307
+ readonly surfaceId: string;
308
+ readonly edge: 'top' | 'right' | 'bottom' | 'left';
309
+ readonly bounds: PlatformViewportBounds;
310
+ }
311
+ /**
312
+ * Structurally matches `TargetViewportUsableAreaInput` in @mpgd/target-config
313
+ * (field names, CSS-pixel units, and edge union), so consumers can pass the
314
+ * same state to `resolveTargetViewportUsableArea` without conversion.
315
+ */
316
+ export interface PlatformViewportState {
317
+ readonly width: number;
318
+ readonly height: number;
319
+ readonly safeAreaInsets: PlatformViewportInsets;
320
+ readonly systemBarInsets: PlatformViewportInsets;
321
+ readonly keyboardInsets: PlatformViewportInsets;
322
+ readonly occupiedSurfaces: readonly PlatformViewportOccupiedSurface[];
323
+ }
324
+ /** Optional native viewport source; consumers use one owner for canvas and DOM layout. */
325
+ export interface ViewportAdapter {
326
+ getState(): PlatformViewportState;
327
+ onChange(callback: (state: PlatformViewportState) => void): () => void;
328
+ dispose?(): void;
329
+ }
270
330
  export interface LifecycleAdapter {
271
331
  onPause(callback: () => void): () => void;
272
332
  onResume(callback: () => void): () => void;
333
+ /**
334
+ * Android handlers run last-registered-first until one returns true. If none
335
+ * consumes the event, Capacitor navigates WebView history when possible or
336
+ * exits the app. The method's presence does not imply support on iOS: check
337
+ * the gateway target. Persist progress at checkpoints; back/exit callbacks
338
+ * are not guaranteed to run before process termination.
339
+ */
340
+ onBackButton?(handler: (event: NativeBackButtonEvent) => boolean | Promise<boolean>): () => void;
341
+ /** Game links and OAuth redirects use separate callbacks. */
342
+ onGameUrlOpen?(callback: (event: NativeOpenUrlEvent) => void): () => void;
343
+ onOAuthRedirect?(callback: (event: NativeOpenUrlEvent) => void): () => void;
344
+ getInitialGameUrl?(): Promise<NativeOpenUrlEvent | null>;
345
+ getInitialOAuthRedirect?(): Promise<NativeOpenUrlEvent | null>;
346
+ /**
347
+ * Keep execution paused while external UI is open. Call the returned
348
+ * idempotent release function when it closes, including failure paths;
349
+ * otherwise execution remains paused.
350
+ */
351
+ beginExternalActivity?(): () => void;
352
+ /** Remove only the native listeners owned by this adapter instance. */
353
+ dispose?(): Promise<void>;
273
354
  }
274
355
  export interface StorageLoadResult {
275
356
  readonly value: unknown;
@@ -284,6 +365,19 @@ export interface StorageAdapter {
284
365
  readonly value: unknown;
285
366
  }): Promise<void>;
286
367
  }
368
+ /** Device-protected opaque credentials; never backed by ordinary game storage. */
369
+ export interface SecureCredentialStore {
370
+ load(input: {
371
+ readonly key: string;
372
+ }): Promise<string | null>;
373
+ save(input: {
374
+ readonly key: string;
375
+ readonly value: string;
376
+ }): Promise<void>;
377
+ remove(input: {
378
+ readonly key: string;
379
+ }): Promise<void>;
380
+ }
287
381
  export interface PlatformGateway {
288
382
  readonly target: PlatformTarget;
289
383
  getCapabilities(): Promise<PlatformCapabilities>;
@@ -292,7 +386,10 @@ export interface PlatformGateway {
292
386
  readonly ads: AdAdapter;
293
387
  readonly leaderboard: LeaderboardAdapter;
294
388
  readonly lifecycle: LifecycleAdapter;
389
+ readonly viewport?: ViewportAdapter;
295
390
  readonly storage: StorageAdapter;
391
+ /** Optional: missing native support must not fall back to plaintext storage. */
392
+ readonly secureCredentials?: SecureCredentialStore;
296
393
  readonly presentation?: PresentationAdapter;
297
394
  readonly sharing?: ShareAdapter;
298
395
  readonly notifications?: NotificationSubscriptionAdapter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/platform",
3
- "version": "0.12.2",
3
+ "version": "0.14.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": {