@mpgd/platform 0.18.0 → 0.20.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,3 +1,4 @@
1
+ import { deriveLegacyCommerceCapabilities } from './commerce-capabilities.js';
1
2
  export const platformCapabilityKeys = Object.freeze([
2
3
  'nativeIap',
3
4
  'nativeAds',
@@ -22,6 +23,7 @@ const allowedPlatformCapabilityKeys = Object.freeze([
22
23
  const allowedPlatformCapabilityKeySet = new Set([
23
24
  ...allowedPlatformCapabilityKeys,
24
25
  'providerAvailability',
26
+ 'commerce',
25
27
  ]);
26
28
  const optionalPlatformCapabilityKeySet = new Set(optionalPlatformCapabilityKeys);
27
29
  const providerFeatureSet = new Set([
@@ -41,6 +43,19 @@ const providerAvailabilitySet = new Set([
41
43
  'temporarily-unavailable',
42
44
  'available',
43
45
  ]);
46
+ // The optional commerce v2 block follows the same rules as the boolean
47
+ // snapshot: exact keys, typed values, and a fresh object on every read.
48
+ const commerceCapabilityBlockKeys = Object.freeze({
49
+ purchase: ['consumable', 'durable', 'subscription', 'quantity'],
50
+ catalog: ['products', 'offers', 'localizedPrice'],
51
+ lifecycle: ['availablePurchases', 'pending', 'restore', 'events', 'finish'],
52
+ authority: ['evidence', 'accountBinding'],
53
+ });
54
+ const commerceCapabilityEnums = Object.freeze({
55
+ 'lifecycle.finish': ['client', 'server', 'host', 'none'],
56
+ 'authority.evidence': ['client-token', 'host-callback', 'adapter-settled', 'server-push'],
57
+ 'authority.accountBinding': ['required', 'optional', 'host'],
58
+ });
44
59
  /**
45
60
  * Runs provider-neutral capability checks against real adapter gateways or
46
61
  * target-configured wrappers. Every read must return a complete boolean
@@ -116,6 +131,55 @@ function assertCapabilitySnapshot(actual, expected) {
116
131
  }
117
132
  assertProviderAvailabilityShape(actual.providerAvailability);
118
133
  assertEqual(normalizedAvailability(actual.providerAvailability), normalizedAvailability(expected.providerAvailability), 'provider availability must match the expected provider state');
134
+ assertCommerceCapabilityBlock(actual, expected.commerce);
135
+ }
136
+ function assertCommerceCapabilityBlock(snapshot, expected) {
137
+ const actual = snapshot.commerce;
138
+ if (actual === undefined) {
139
+ assert(expected === undefined, 'commerce capabilities must be present when the fixture expects them');
140
+ return;
141
+ }
142
+ assert(expected !== undefined, 'commerce capabilities must be absent when the fixture omits them');
143
+ assert(typeof actual === 'object' && actual !== null && !Array.isArray(actual), 'commerce capabilities must be a non-array record');
144
+ const block = actual;
145
+ const blockKeys = Object.keys(block).sort();
146
+ assertEqual(JSON.stringify(blockKeys), JSON.stringify([...Object.keys(commerceCapabilityBlockKeys), 'availability'].sort()), 'commerce capabilities must contain exactly purchase, catalog, lifecycle, authority, and availability');
147
+ for (const [group, keys] of Object.entries(commerceCapabilityBlockKeys)) {
148
+ const nested = block[group];
149
+ assert(typeof nested === 'object' && nested !== null && !Array.isArray(nested), `commerce ${group} capabilities must be a non-array record`);
150
+ const record = nested;
151
+ assertEqual(JSON.stringify(Object.keys(record).sort()), JSON.stringify([...keys].sort()), `commerce ${group} capabilities must contain exactly ${keys.join(', ')}`);
152
+ for (const key of keys) {
153
+ const path = `${group}.${key}`;
154
+ const allowed = commerceCapabilityEnums[path];
155
+ if (allowed === undefined) {
156
+ assertEqual(typeof record[key], 'boolean', `commerce capability ${path} must be a boolean`);
157
+ }
158
+ else {
159
+ assert(allowed.includes(record[key]), `commerce capability ${path} must be one of ${allowed.join(', ')}`);
160
+ }
161
+ }
162
+ }
163
+ assert(providerAvailabilitySet.has(block.availability), 'commerce availability must be a known readiness state');
164
+ assertEqual(normalizedCommerce(block), normalizedCommerce(expected), 'commerce capabilities must match the expected provider state');
165
+ const legacy = deriveLegacyCommerceCapabilities(block);
166
+ assertEqual(snapshot.nativeIap, legacy.nativeIap, 'nativeIap must equal the value derived from the commerce capabilities');
167
+ assertEqual(snapshot.subscriptionIap ?? false, legacy.subscriptionIap, 'subscriptionIap must equal the value derived from the commerce capabilities');
168
+ }
169
+ function normalizedCommerce(value) {
170
+ if (value === undefined) {
171
+ return 'undefined';
172
+ }
173
+ return JSON.stringify([
174
+ ['purchase', sortedEntries(value.purchase)],
175
+ ['catalog', sortedEntries(value.catalog)],
176
+ ['lifecycle', sortedEntries(value.lifecycle)],
177
+ ['authority', sortedEntries(value.authority)],
178
+ ['availability', value.availability],
179
+ ]);
180
+ }
181
+ function sortedEntries(value) {
182
+ return Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
119
183
  }
120
184
  function assertProviderAvailabilityShape(value) {
121
185
  if (value === undefined) {
@@ -133,6 +197,17 @@ function cloneExpectedCapabilities(value) {
133
197
  ...(value.providerAvailability === undefined
134
198
  ? {}
135
199
  : { providerAvailability: { ...value.providerAvailability } }),
200
+ ...(value.commerce === undefined
201
+ ? {}
202
+ : {
203
+ commerce: {
204
+ purchase: { ...value.commerce.purchase },
205
+ catalog: { ...value.commerce.catalog },
206
+ lifecycle: { ...value.commerce.lifecycle },
207
+ authority: { ...value.commerce.authority },
208
+ availability: value.commerce.availability,
209
+ },
210
+ }),
136
211
  };
137
212
  }
138
213
  function normalizedAvailability(value) {
@@ -156,12 +231,39 @@ function tryMutateSnapshot(snapshot) {
156
231
  // A frozen nested readiness record already satisfies isolation.
157
232
  }
158
233
  }
234
+ if (snapshot.commerce !== undefined) {
235
+ tryMutateCommerceSnapshot(snapshot.commerce);
236
+ }
237
+ }
238
+ // Every nested block is probed: a gateway may rebuild the outer object while
239
+ // reusing one mutable nested record across reads.
240
+ function tryMutateCommerceSnapshot(commerce) {
241
+ const probes = [
242
+ [commerce.purchase, 'consumable', !commerce.purchase.consumable],
243
+ [commerce.catalog, 'products', !commerce.catalog.products],
244
+ [commerce.lifecycle, 'events', !commerce.lifecycle.events],
245
+ [
246
+ commerce.authority,
247
+ 'accountBinding',
248
+ commerce.authority.accountBinding === 'host' ? 'optional' : 'host',
249
+ ],
250
+ [commerce, 'availability', commerce.availability === 'available' ? 'unsupported' : 'available'],
251
+ ];
252
+ for (const [target, key, next] of probes) {
253
+ try {
254
+ Reflect.set(target, key, next);
255
+ }
256
+ catch {
257
+ // A frozen nested commerce block already satisfies isolation.
258
+ }
259
+ }
159
260
  }
160
261
  function hasCapabilityDifference(first, second) {
161
262
  return allowedPlatformCapabilityKeys
162
263
  .some((key) => (first[key] ?? false) !== (second[key] ?? false))
163
264
  || normalizedAvailability(first.providerAvailability)
164
- !== normalizedAvailability(second.providerAvailability);
265
+ !== normalizedAvailability(second.providerAvailability)
266
+ || normalizedCommerce(first.commerce) !== normalizedCommerce(second.commerce);
165
267
  }
166
268
  function assert(condition, message) {
167
269
  if (!condition) {
@@ -0,0 +1,9 @@
1
+ import type { CommerceCapabilities } from './index.js';
2
+ export interface LegacyCommerceCapabilities {
3
+ readonly nativeIap: boolean;
4
+ readonly subscriptionIap: boolean;
5
+ }
6
+ /** Transition rule for the boolean `PlatformCapabilities` flags. */
7
+ export declare function deriveLegacyCommerceCapabilities(capabilities: CommerceCapabilities): LegacyCommerceCapabilities;
8
+ /** A fresh all-false snapshot; callers must not share or mutate a returned object. */
9
+ export declare function createUnsupportedCommerceCapabilities(): CommerceCapabilities;
@@ -0,0 +1,24 @@
1
+ /** Transition rule for the boolean `PlatformCapabilities` flags. */
2
+ export function deriveLegacyCommerceCapabilities(capabilities) {
3
+ return {
4
+ nativeIap: capabilities.availability === 'available'
5
+ && (capabilities.purchase.consumable || capabilities.purchase.durable),
6
+ subscriptionIap: capabilities.purchase.subscription,
7
+ };
8
+ }
9
+ /** A fresh all-false snapshot; callers must not share or mutate a returned object. */
10
+ export function createUnsupportedCommerceCapabilities() {
11
+ return {
12
+ purchase: { consumable: false, durable: false, subscription: false, quantity: false },
13
+ catalog: { products: false, offers: false, localizedPrice: false },
14
+ lifecycle: {
15
+ availablePurchases: false,
16
+ pending: false,
17
+ restore: false,
18
+ events: false,
19
+ finish: 'none',
20
+ },
21
+ authority: { evidence: 'client-token', accountBinding: 'optional' },
22
+ availability: 'unsupported',
23
+ };
24
+ }