@mpgd/adapter-capacitor 0.7.5 → 0.9.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.js CHANGED
@@ -1,15 +1,21 @@
1
1
  import { decodeBridgeCredentialLoadData, decodeBridgeStorageLoadData, } from '@mpgd/bridge';
2
2
  import { CapacitorGameServices } from '@mpgd/capacitor-game-services';
3
3
  import { PlatformOperationError, } from '@mpgd/platform';
4
+ import { deriveLegacyCommerceCapabilities } from '@mpgd/platform/commerce';
4
5
  import { createCapacitorAppEvents, } from './app-events.js';
6
+ import { createBridgeCommerceProvider } from './commerce-bridge.js';
5
7
  import { assertProviderResponseData, createCapacitorProviderRegistry, } from './providers.js';
6
8
  import { createCapacitorViewport } from './viewport.js';
7
- export { createCapacitorProviderRegistry, } from './providers.js';
9
+ export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, deriveCommerceFeatureStates, } from './providers.js';
10
+ export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, } from './commerce-bridge.js';
11
+ export { createStoreCommerceProvider, recoverableErrorCodes, } from './store-commerce-provider.js';
12
+ export { defaultStoreSdkErrorCodeMap, normalizeStoreSdkError, } from './store-sdk.js';
8
13
  export { createCapacitorAppEvents, } from './app-events.js';
9
14
  export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, } from './native-http.js';
10
15
  export { createCapacitorViewport, } from './viewport.js';
11
16
  const externalActivityMethods = new Set([
12
17
  'commerce.purchase',
18
+ 'commerce.v2.requestPurchase',
13
19
  'ads.showRewarded',
14
20
  'ads.showInterstitial',
15
21
  'identity.requestUpgrade',
@@ -18,6 +24,12 @@ const externalActivityMethods = new Set([
18
24
  function isRecord(value) {
19
25
  return typeof value === 'object' && value !== null && !Array.isArray(value);
20
26
  }
27
+ /**
28
+ * Upper bound on how long a `launched` commerce v2 purchase keeps the game
29
+ * paused when no purchase event for the product arrives (for example a store
30
+ * sheet dismissed without a callback).
31
+ */
32
+ export const launchedPurchaseActivityTimeoutMs = 120_000;
21
33
  const safeFallbackMethods = new Set([
22
34
  'identity.getPlayer',
23
35
  'identity.getSession',
@@ -46,6 +58,17 @@ export function createCapacitorPlatformGateway(input) {
46
58
  const providers = createCapacitorProviderRegistry(input.providers ?? []);
47
59
  const adProvider = providers.byMethod.get('ads.showRewarded')?.adProvider
48
60
  ?? providers.byMethod.get('ads.showInterstitial')?.adProvider;
61
+ const commerceProvider = providers.commerce;
62
+ // The v2 surface is served through the same request path as every other
63
+ // provider method, so the bridge envelope, response validation, and external
64
+ // activity bookkeeping are exercised in process exactly as over a transport.
65
+ const commerceV2 = commerceProvider === undefined
66
+ ? undefined
67
+ : createBridgeCommerceProvider({
68
+ store: commerceProvider.commerce.store,
69
+ request: (method, payload) => request(method, payload),
70
+ subscribe: (listener) => commerceProvider.bridge.subscribe(listener),
71
+ });
49
72
  const lifecycle = createCapacitorAppEvents({
50
73
  target: input.target,
51
74
  ...(input.app === undefined ? {} : { app: input.app }),
@@ -59,10 +82,14 @@ export function createCapacitorPlatformGateway(input) {
59
82
  const viewport = input.viewport ?? createCapacitorViewport({
60
83
  ...(input.onAppEventError === undefined ? {} : { onError: input.onAppEventError }),
61
84
  });
85
+ const launchedPurchaseHolds = new Set();
62
86
  const gatewayLifecycle = {
63
87
  ...lifecycle,
64
88
  async dispose() {
65
89
  try {
90
+ for (const finish of [...launchedPurchaseHolds]) {
91
+ finish();
92
+ }
66
93
  await lifecycle.dispose?.();
67
94
  }
68
95
  finally {
@@ -76,6 +103,7 @@ export function createCapacitorPlatformGateway(input) {
76
103
  const releaseExternalActivity = externalActivityMethods.has(method)
77
104
  ? lifecycle.beginExternalActivity?.()
78
105
  : undefined;
106
+ let holdActivity = false;
79
107
  try {
80
108
  const provider = leaderboardRoute === 'remote'
81
109
  && (method === 'leaderboard.submitScore' || method === 'leaderboard.open')
@@ -98,7 +126,17 @@ export function createCapacitorPlatformGateway(input) {
98
126
  }
99
127
  try {
100
128
  await providers.assertMethodReady(method, payload, productType);
101
- return await sendRequest(method, payload, provider);
129
+ const data = await sendRequest(method, payload, provider);
130
+ if (releaseExternalActivity !== undefined
131
+ && method === 'commerce.v2.requestPurchase'
132
+ && isRecord(data) && data.outcome === 'launched') {
133
+ // The store UI is still open: stay paused until this product's
134
+ // purchase event arrives or the safety timeout ends. The v1 path
135
+ // has no event to wait for and releases when its request returns.
136
+ holdActivity = true;
137
+ holdLaunchedPurchaseActivity(releaseExternalActivity, isRecord(payload) ? payload.logicalId : undefined);
138
+ }
139
+ return data;
102
140
  }
103
141
  catch (error) {
104
142
  // Only non-mutating guest/status queries may degrade. A stale readiness
@@ -112,7 +150,53 @@ export function createCapacitorPlatformGateway(input) {
112
150
  return await sendRequest(method, payload, undefined);
113
151
  }
114
152
  finally {
115
- releaseExternalActivity?.();
153
+ if (!holdActivity) {
154
+ releaseExternalActivity?.();
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Keeps the external activity of a `launched` purchase open until the
160
+ * provider reports that product (`purchaseUpdated` or `purchaseError`, or an
161
+ * event without a logical id), the safety timeout ends, or the gateway is
162
+ * disposed. Every path releases exactly once.
163
+ */
164
+ function holdLaunchedPurchaseActivity(release, logicalId) {
165
+ let done = false;
166
+ let unsubscribe;
167
+ const finish = () => {
168
+ if (done) {
169
+ return;
170
+ }
171
+ done = true;
172
+ launchedPurchaseHolds.delete(finish);
173
+ clearTimeout(timer);
174
+ unsubscribe?.();
175
+ release();
176
+ };
177
+ launchedPurchaseHolds.add(finish);
178
+ const timer = setTimeout(finish, launchedPurchaseActivityTimeoutMs);
179
+ if (commerceProvider === undefined) {
180
+ return;
181
+ }
182
+ const subscription = commerceProvider.bridge.subscribe(({ event }) => {
183
+ if (event.type === 'subscriptionUpdated') {
184
+ return;
185
+ }
186
+ const eventLogicalId = event.type === 'purchaseUpdated'
187
+ ? event.purchase.logicalId
188
+ : event.logicalId;
189
+ if (eventLogicalId === undefined || typeof logicalId !== 'string'
190
+ || eventLogicalId === logicalId) {
191
+ finish();
192
+ }
193
+ });
194
+ // A buffered event may have finished the hold while subscribing.
195
+ if (done) {
196
+ subscription();
197
+ }
198
+ else {
199
+ unsubscribe = subscription;
116
200
  }
117
201
  }
118
202
  async function sendRequest(method, payload, provider) {
@@ -161,22 +245,28 @@ export function createCapacitorPlatformGateway(input) {
161
245
  target: input.target,
162
246
  async getCapabilities() {
163
247
  const base = await request('runtime.getCapabilities', {});
164
- const states = await providers.getAvailability();
248
+ // One bounded registry read serves the feature states and the commerce
249
+ // block, so a stalled or invalid provider degrades the snapshot instead
250
+ // of rejecting it, and the boolean flags always follow the block.
251
+ const { availability: states, commerce } = await providers.getCapabilitySnapshot();
165
252
  const rewardedAds = states.rewardedAds === 'available';
166
253
  const interstitialAds = states.interstitialAds === 'available';
167
254
  const bannerAds = states.bannerAds === 'available';
255
+ const legacy = commerce === undefined ? undefined : deriveLegacyCommerceCapabilities(commerce);
168
256
  return {
169
257
  ...base,
170
- nativeIap: states.nativeIap === 'available',
171
- subscriptionIap: states.subscriptionIap === 'available',
258
+ nativeIap: legacy?.nativeIap ?? states.nativeIap === 'available',
259
+ subscriptionIap: legacy?.subscriptionIap ?? states.subscriptionIap === 'available',
172
260
  nativeAds: rewardedAds || interstitialAds || bannerAds,
173
261
  rewardedAds,
174
262
  interstitialAds,
175
263
  bannerAds,
176
264
  nativeLeaderboard: states.nativeLeaderboard === 'available',
177
265
  providerAvailability: states,
266
+ ...(commerce === undefined ? {} : { commerce }),
178
267
  };
179
268
  },
269
+ ...(commerceV2 === undefined ? {} : { commerceV2 }),
180
270
  identity: {
181
271
  getPlayer: () => request('identity.getPlayer', {}),
182
272
  getSession: () => request('identity.getSession', {}),
@@ -1,8 +1,14 @@
1
- import type { BridgeMethod, BridgeRequest, BridgeResponse } from '@mpgd/bridge';
1
+ import { type BridgeEvent, type BridgeMethod, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
2
2
  import type { AdProvider } from '@mpgd/platform/ads';
3
- import { type PlatformProviderAvailability, type PlatformProviderFeature, type ProductType } from '@mpgd/platform';
3
+ import { type CommerceCapabilities, type PlatformProviderAvailability, type PlatformProviderFeature, type ProductType } from '@mpgd/platform';
4
+ import { type CommerceProvider } from '@mpgd/platform/commerce';
4
5
  export interface NativeBridge {
5
6
  request(input: BridgeRequest): Promise<BridgeResponse>;
7
+ /**
8
+ * Optional commerce v2 event delivery (`{ protocol: 'mpgd.commerce.v2', sequence, event }`).
9
+ * B1 wires it for store providers; the base gateway works without it.
10
+ */
11
+ subscribe?(listener: (event: BridgeEvent) => void): () => void;
6
12
  }
7
13
  /**
8
14
  * An optional, separately installed provider handles only its declared bridge
@@ -12,17 +18,61 @@ export interface NativeBridge {
12
18
  */
13
19
  export interface CapacitorServiceProvider {
14
20
  readonly id: string;
21
+ /**
22
+ * Serves the declared `methods`. For a provider with `commerce`, it also
23
+ * serves the ten commerce bridge methods and must implement `subscribe`;
24
+ * `createCommerceServiceProvider` builds one from a `CommerceProvider`.
25
+ */
15
26
  readonly bridge: NativeBridge;
16
27
  readonly features: readonly PlatformProviderFeature[];
28
+ /**
29
+ * Declared v1 bridge methods. A provider with `commerce` declares no
30
+ * `commerce.*` method: the four v1 commerce methods and the six
31
+ * `commerce.v2.*` methods are implied and owned by the v2 provider.
32
+ */
17
33
  readonly methods: readonly BridgeMethod[];
18
34
  /** Optional native event surface. The base adapter never imports its SDK. */
19
35
  readonly adProvider?: AdProvider;
36
+ /**
37
+ * Optional commerce v2 provider (`createStoreCommerceProvider`). Its
38
+ * `getCapabilities()` is the readiness of the declared `nativeIap` and
39
+ * `subscriptionIap` features; `getAvailability` covers the other features.
40
+ */
41
+ readonly commerce?: CommerceProvider;
20
42
  getAvailability(): Promise<Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>>;
21
43
  }
44
+ /** The provider the registry serves commerce v2 from. */
45
+ export interface CapacitorCommerceServiceProvider extends CapacitorServiceProvider {
46
+ readonly commerce: CommerceProvider;
47
+ readonly bridge: NativeBridge & Required<Pick<NativeBridge, 'subscribe'>>;
48
+ }
49
+ export interface CapacitorProviderCapabilitySnapshot {
50
+ readonly availability: Readonly<Record<PlatformProviderFeature, PlatformProviderAvailability>>;
51
+ /**
52
+ * Present when a commerce v2 provider is registered: its validated
53
+ * `getCapabilities()` answer from the same bounded read that produced the
54
+ * `nativeIap` and `subscriptionIap` states, or a degraded block
55
+ * (`availability: 'temporarily-unavailable'`, nothing sold) when that read
56
+ * stalled, failed, or returned a shape the contract rejects.
57
+ */
58
+ readonly commerce?: CommerceCapabilities;
59
+ }
22
60
  export interface CapacitorProviderRegistry {
23
61
  readonly byMethod: ReadonlyMap<BridgeMethod, CapacitorServiceProvider>;
62
+ /** The provider that serves commerce v2, when one is registered. */
63
+ readonly commerce?: CapacitorCommerceServiceProvider;
64
+ /** Feature readiness plus the commerce block, from one bounded read per provider. */
65
+ getCapabilitySnapshot(): Promise<CapacitorProviderCapabilitySnapshot>;
24
66
  assertMethodReady(method: BridgeMethod, payload?: unknown, productType?: ProductType): Promise<void>;
25
67
  getAvailability(): Promise<Readonly<Record<PlatformProviderFeature, PlatformProviderAvailability>>>;
26
68
  }
27
69
  export declare function createCapacitorProviderRegistry(input: readonly CapacitorServiceProvider[]): CapacitorProviderRegistry;
28
70
  export declare function assertProviderResponseData(method: BridgeMethod, data: unknown): void;
71
+ /** The commerce block a gateway reports while its provider cannot be read. */
72
+ export declare function createDegradedCommerceCapabilities(): CommerceCapabilities;
73
+ /**
74
+ * Readiness of the commerce features a v2 provider declares, from its own
75
+ * capability declaration: a feature it does not sell is `unsupported`, and a
76
+ * read that failed or stalled is `temporarily-unavailable`.
77
+ */
78
+ export declare function deriveCommerceFeatureStates(capabilities: CommerceCapabilities | undefined, features: readonly PlatformProviderFeature[]): Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>;
package/dist/providers.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { bridgeCommerceV2Methods, isBridgeCommerceV2Method, } from '@mpgd/bridge';
1
2
  import { isAdMobClientRewardEvidence } from '@mpgd/game-services/admob-client-reward';
2
3
  import { PlatformOperationError, } from '@mpgd/platform';
4
+ import { createUnsupportedCommerceCapabilities, isCommerceCapabilities, } from '@mpgd/platform/commerce';
5
+ import { assertCommerceBridgeResult } from './commerce-bridge.js';
3
6
  const featureMethods = {
4
7
  nativeIap: [
5
8
  'commerce.getProducts',
@@ -22,6 +25,12 @@ const featureMethods = {
22
25
  };
23
26
  const providerFeatures = Object.keys(featureMethods);
24
27
  const providerMethodSet = new Set(Object.values(featureMethods).flat());
28
+ const commerceFeatures = ['nativeIap', 'subscriptionIap'];
29
+ const v1CommerceMethods = featureMethods.nativeIap;
30
+ const impliedCommerceMethods = [
31
+ ...v1CommerceMethods,
32
+ ...bridgeCommerceV2Methods,
33
+ ];
25
34
  const availabilitySet = new Set([
26
35
  'unsupported',
27
36
  'configuration-required',
@@ -40,12 +49,14 @@ export function createCapacitorProviderRegistry(input) {
40
49
  const byMethod = new Map();
41
50
  const byFeature = new Map();
42
51
  const ids = new Set();
52
+ let commerce;
43
53
  for (const provider of providers) {
44
54
  if (provider.id.trim() === '' || ids.has(provider.id)) {
45
55
  throw new Error(`Capacitor provider ID is empty or registered twice: ${provider.id}`);
46
56
  }
47
57
  ids.add(provider.id);
48
- if (provider.features.length === 0 || provider.methods.length === 0) {
58
+ if (provider.features.length === 0
59
+ || (provider.methods.length === 0 && provider.commerce === undefined)) {
49
60
  throw new Error(`Capacitor provider ${provider.id} must declare features and methods.`);
50
61
  }
51
62
  const methods = new Set(provider.methods);
@@ -53,19 +64,44 @@ export function createCapacitorProviderRegistry(input) {
53
64
  throw new Error(`Capacitor provider ${provider.id} repeats a bridge method.`);
54
65
  }
55
66
  for (const method of methods) {
67
+ if (isBridgeCommerceV2Method(method)) {
68
+ // A v2 provider is registered through `commerce`; its bridge methods are implied.
69
+ throw new Error(`Capacitor provider ${provider.id} cannot declare commerce v2 method ${method}; `
70
+ + 'register a v2 provider through `commerce` instead, commerce v2 is unavailable as '
71
+ + 'a declared method.');
72
+ }
56
73
  if (!providerMethodSet.has(method)) {
57
74
  throw new Error(`Capacitor provider ${provider.id} cannot replace base method ${method}.`);
58
75
  }
76
+ if (provider.commerce !== undefined && v1CommerceMethods.includes(method)) {
77
+ throw new Error(`Capacitor provider ${provider.id} cannot declare both a commerce v2 provider and `
78
+ + `v1 method ${method}; the registry serves v1 callers from the v2 provider.`);
79
+ }
80
+ }
81
+ const implied = provider.commerce === undefined ? [] : impliedCommerceMethods;
82
+ for (const method of [...methods, ...implied]) {
59
83
  if (byMethod.has(method)) {
60
84
  throw new Error(`Capacitor bridge method ${method} has multiple providers.`);
61
85
  }
62
86
  }
87
+ const owned = new Set([...methods, ...implied]);
88
+ if (provider.commerce !== undefined) {
89
+ if (!provider.features.some((feature) => commerceFeatures.includes(feature))) {
90
+ throw new Error(`Capacitor provider ${provider.id} declares a commerce v2 provider but no commerce feature.`);
91
+ }
92
+ if (provider.bridge.subscribe === undefined) {
93
+ // Without the event surface a purchase that completes after the store
94
+ // UI closed would never reach the reconciler.
95
+ throw new Error(`Capacitor provider ${provider.id} declares a commerce v2 provider but its bridge has no `
96
+ + 'subscribe; build it with createCommerceServiceProvider.');
97
+ }
98
+ }
63
99
  for (const feature of provider.features) {
64
100
  const required = featureMethods[feature];
65
101
  if (required === undefined || byFeature.has(feature)) {
66
102
  throw new Error(`Capacitor feature ${feature} has multiple or invalid providers.`);
67
103
  }
68
- if (required.some((method) => !methods.has(method))) {
104
+ if (required.some((method) => !owned.has(method))) {
69
105
  throw new Error(`Capacitor provider ${provider.id} lacks methods for ${feature}.`);
70
106
  }
71
107
  byFeature.set(feature, provider);
@@ -73,67 +109,80 @@ export function createCapacitorProviderRegistry(input) {
73
109
  if ([...methods].some((method) => !provider.features.some((feature) => featureMethods[feature].includes(method)))) {
74
110
  throw new Error(`Capacitor provider ${provider.id} declares an unowned method.`);
75
111
  }
76
- for (const method of methods) {
112
+ for (const method of owned) {
77
113
  byMethod.set(method, provider);
78
114
  }
115
+ if (provider.commerce !== undefined) {
116
+ // Validated above: the bridge implements `subscribe`.
117
+ commerce = provider;
118
+ }
79
119
  }
80
120
  const readProviderStates = async (provider) => {
81
- let reported;
82
- let timer;
83
- try {
84
- reported = await Promise.race([
85
- Promise.resolve().then(() => provider.getAvailability()),
86
- new Promise((_resolve, reject) => {
87
- timer = setTimeout(() => reject(new Error('Provider availability timed out.')), availabilityTimeoutMs);
88
- }),
89
- ]);
90
- if (!isRecord(reported)) {
91
- throw new Error('Invalid provider availability.');
92
- }
93
- }
94
- catch {
95
- reported = {};
121
+ const states = {};
122
+ const commerceOwned = provider.commerce === undefined
123
+ ? []
124
+ : provider.features.filter((feature) => commerceFeatures.includes(feature));
125
+ const bridged = provider.features.filter((feature) => !commerceOwned.includes(feature));
126
+ let commerceBlock;
127
+ if (provider.commerce !== undefined && commerceOwned.length > 0) {
128
+ commerceBlock = await readCommerceCapabilities(provider.commerce);
129
+ Object.assign(states, deriveCommerceFeatureStates(commerceBlock, commerceOwned));
96
130
  }
97
- finally {
98
- if (timer !== undefined) {
99
- clearTimeout(timer);
131
+ if (bridged.length > 0) {
132
+ const reported = await readWithTimeout(Promise.resolve().then(() => provider.getAvailability()));
133
+ for (const feature of bridged) {
134
+ const state = isRecord(reported) ? reported[feature] : undefined;
135
+ states[feature] = availabilitySet.has(state)
136
+ ? state
137
+ : 'temporarily-unavailable';
100
138
  }
101
139
  }
102
- const states = {};
103
- for (const feature of provider.features) {
104
- const state = reported[feature];
105
- states[feature] = availabilitySet.has(state)
106
- ? state
107
- : 'temporarily-unavailable';
108
- }
109
- return states;
140
+ return { states, ...(commerceBlock === undefined ? {} : { commerce: commerceBlock }) };
110
141
  };
111
- const getAvailability = async () => {
142
+ const getCapabilitySnapshot = async () => {
112
143
  const states = {};
113
144
  for (const feature of providerFeatures) {
114
145
  states[feature] = 'unsupported';
115
146
  }
147
+ let commerceBlock;
116
148
  const snapshots = await Promise.all(providers.map(readProviderStates));
117
149
  for (const snapshot of snapshots) {
118
- Object.assign(states, snapshot);
150
+ Object.assign(states, snapshot.states);
151
+ commerceBlock = snapshot.commerce ?? commerceBlock;
152
+ }
153
+ if (commerce !== undefined && commerceBlock === undefined) {
154
+ // One bounded read serves the feature states and the block, so a stalled,
155
+ // failed, or invalid provider degrades both the same way.
156
+ commerceBlock = createDegradedCommerceCapabilities();
119
157
  }
120
- return states;
158
+ return {
159
+ availability: states,
160
+ ...(commerceBlock === undefined ? {} : { commerce: commerceBlock }),
161
+ };
121
162
  };
163
+ const getAvailability = async () => (await getCapabilitySnapshot()).availability;
122
164
  return {
123
165
  byMethod,
166
+ ...(commerce === undefined ? {} : { commerce }),
124
167
  getAvailability,
168
+ getCapabilitySnapshot,
125
169
  async assertMethodReady(method, payload, productType) {
126
170
  const provider = byMethod.get(method);
127
171
  if (provider === undefined) {
128
172
  return;
129
173
  }
174
+ if (isBridgeCommerceV2Method(method)) {
175
+ // The v2 provider reads its own `availability` and fails closed before
176
+ // any store UI opens; the registry adds no second, stale snapshot.
177
+ return;
178
+ }
130
179
  if (method === 'ads.unmountBanner') {
131
180
  // Teardown must reach the provider that mounted the banner even after
132
181
  // its serving state becomes unavailable.
133
182
  return;
134
183
  }
135
184
  // An unrelated provider must not delay a purchase, ad, or identity call.
136
- const states = await readProviderStates(provider);
185
+ const { states } = await readProviderStates(provider);
137
186
  let relevant = provider.features.filter((feature) => featureMethods[feature].includes(method));
138
187
  if (method === 'commerce.purchase' && relevant.length > 1) {
139
188
  relevant = relevant.filter((feature) => feature === (productType === 'subscription' ? 'subscriptionIap'
@@ -198,6 +247,73 @@ export function assertProviderResponseData(method, data) {
198
247
  });
199
248
  }
200
249
  }
250
+ /**
251
+ * One bounded, validated capability read. A stalled, throwing, or invalid
252
+ * provider yields `undefined`; the block is copied so the gateway never hands
253
+ * out a provider-owned object.
254
+ */
255
+ async function readCommerceCapabilities(provider) {
256
+ const reported = await readWithTimeout(Promise.resolve().then(() => provider.getCapabilities()));
257
+ if (!isCommerceCapabilities(reported)) {
258
+ return undefined;
259
+ }
260
+ return {
261
+ purchase: { ...reported.purchase },
262
+ catalog: { ...reported.catalog },
263
+ lifecycle: { ...reported.lifecycle },
264
+ authority: { ...reported.authority },
265
+ availability: reported.availability,
266
+ };
267
+ }
268
+ /** The commerce block a gateway reports while its provider cannot be read. */
269
+ export function createDegradedCommerceCapabilities() {
270
+ return { ...createUnsupportedCommerceCapabilities(), availability: 'temporarily-unavailable' };
271
+ }
272
+ async function readWithTimeout(read) {
273
+ let timer;
274
+ try {
275
+ return await Promise.race([
276
+ read,
277
+ new Promise((_resolve, reject) => {
278
+ timer = setTimeout(() => reject(new Error('Provider availability timed out.')), availabilityTimeoutMs);
279
+ }),
280
+ ]);
281
+ }
282
+ catch {
283
+ return undefined;
284
+ }
285
+ finally {
286
+ if (timer !== undefined) {
287
+ clearTimeout(timer);
288
+ }
289
+ }
290
+ }
291
+ /**
292
+ * Readiness of the commerce features a v2 provider declares, from its own
293
+ * capability declaration: a feature it does not sell is `unsupported`, and a
294
+ * read that failed or stalled is `temporarily-unavailable`.
295
+ */
296
+ export function deriveCommerceFeatureStates(capabilities, features) {
297
+ const states = {};
298
+ const availability = capabilities === undefined
299
+ || !availabilitySet.has(capabilities.availability)
300
+ ? 'temporarily-unavailable'
301
+ : capabilities.availability;
302
+ for (const feature of features) {
303
+ if (feature === 'nativeIap') {
304
+ states.nativeIap = capabilities !== undefined
305
+ && !(capabilities.purchase.consumable || capabilities.purchase.durable)
306
+ ? 'unsupported'
307
+ : availability;
308
+ }
309
+ else if (feature === 'subscriptionIap') {
310
+ states.subscriptionIap = capabilities !== undefined && !capabilities.purchase.subscription
311
+ ? 'unsupported'
312
+ : availability;
313
+ }
314
+ }
315
+ return states;
316
+ }
201
317
  function isRecord(value) {
202
318
  return typeof value === 'object' && value !== null && !Array.isArray(value);
203
319
  }
@@ -211,6 +327,15 @@ function isVoidData(value) {
211
327
  return value === undefined || value === null || (isRecord(value) && Object.keys(value).length === 0);
212
328
  }
213
329
  function isValidProviderData(method, value) {
330
+ if (isBridgeCommerceV2Method(method)) {
331
+ try {
332
+ assertCommerceBridgeResult(method, value);
333
+ return true;
334
+ }
335
+ catch {
336
+ return false;
337
+ }
338
+ }
214
339
  switch (method) {
215
340
  case 'commerce.getProducts':
216
341
  return Array.isArray(value) && value.every((product) => isRecord(product)
@@ -0,0 +1,55 @@
1
+ import type { PlatformEvidenceEnvelope } from '@mpgd/platform';
2
+ import { type CommerceAuthorityCapabilities, type CommerceCatalogCapabilities, type CommerceErrorCode, type CommerceLifecycleCapabilities, type CommerceLogicalProductId, type CommerceProvider, type CommercePurchaseCapabilities, type PurchaseKind, type StoreId, type StorePurchaseAccountBinding } from '@mpgd/platform/commerce';
3
+ import { type StoreSdk, type StoreSdkErrorCodeMap, type StoreSdkPurchase } from './store-sdk.js';
4
+ /** One catalog product sold through the store under `storeSku`. */
5
+ export interface StoreCommerceProductMapping {
6
+ readonly logicalId: CommerceLogicalProductId;
7
+ readonly storeSku: string;
8
+ readonly kind: PurchaseKind;
9
+ /** Default store offer when the request names none. */
10
+ readonly offerId?: string;
11
+ }
12
+ /** Deep-partial overrides applied over the capabilities composed from the SDK declaration. */
13
+ export interface StoreCommerceCapabilityOverrides {
14
+ readonly purchase?: Partial<CommercePurchaseCapabilities>;
15
+ readonly catalog?: Partial<CommerceCatalogCapabilities>;
16
+ readonly lifecycle?: Partial<CommerceLifecycleCapabilities>;
17
+ }
18
+ export interface CreateStoreCommerceProviderInput {
19
+ readonly sdk: StoreSdk;
20
+ /** Defaults to `sdk.store`. */
21
+ readonly store?: StoreId;
22
+ readonly products: readonly StoreCommerceProductMapping[];
23
+ /** Evidence schema the backend registry routes on, for example `google-play.product-purchase.v2`. */
24
+ readonly evidenceSchema: string;
25
+ /**
26
+ * Evidence payload for a purchase. Defaults to the token, transaction id,
27
+ * and SKU; a store provider replaces it to match its backend verifier.
28
+ */
29
+ readonly evidencePayload?: (purchase: StoreSdkPurchase) => PlatformEvidenceEnvelope['payload'];
30
+ /** Resolves the player's store account binding before a purchase opens. */
31
+ readonly accountBinding?: () => Promise<StorePurchaseAccountBinding | undefined>;
32
+ readonly capabilities?: StoreCommerceCapabilityOverrides;
33
+ readonly authority: CommerceAuthorityCapabilities;
34
+ /** Per-store error codes merged over `defaultStoreSdkErrorCodeMap`. */
35
+ readonly mapError?: StoreSdkErrorCodeMap;
36
+ }
37
+ export interface StoreCommerceProvider extends CommerceProvider {
38
+ /** Detaches the SDK listeners; buffered events are dropped. */
39
+ dispose(): void;
40
+ }
41
+ /**
42
+ * `CommerceProvider` over an in-process `StoreSdk`. The provider maps logical
43
+ * ids to store SKUs both ways, composes `CommerceCapabilities` from the SDK
44
+ * declaration, builds `StorePurchase` evidence, buffers SDK events until the
45
+ * first subscriber, and normalizes every SDK failure into a `CommerceError`.
46
+ * It never grants and never sets `settlement`; the backend ledger decides.
47
+ */
48
+ export declare function createStoreCommerceProvider(input: CreateStoreCommerceProviderInput): StoreCommerceProvider;
49
+ /**
50
+ * Codes that mean an unfinished purchase may exist (RFC "Errors": reconcile,
51
+ * never reopen the store UI blindly). The attempt stays `launched` so the
52
+ * reconciler resolves it from `getAvailablePurchases`; a terminal `failed`
53
+ * would exclude it from recovery.
54
+ */
55
+ export declare const recoverableErrorCodes: ReadonlySet<CommerceErrorCode>;