@mpgd/platform 0.22.0 → 0.23.1
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/capability-conformance.d.ts +6 -5
- package/dist/capability-conformance.js +178 -42
- package/dist/commerce-capabilities.d.ts +0 -6
- package/dist/commerce-capabilities.js +2 -8
- package/dist/commerce-contract.d.ts +12 -2
- package/dist/commerce.d.ts +8 -17
- package/dist/commerce.js +10 -94
- package/dist/index.d.ts +80 -43
- package/dist/index.js +3 -36
- package/dist/notifications-conformance.d.ts +529 -0
- package/dist/notifications-conformance.js +3537 -0
- package/dist/notifications-contract.d.ts +204 -0
- package/dist/notifications-contract.js +1 -0
- package/dist/notifications-policy.d.ts +885 -0
- package/dist/notifications-policy.js +1658 -0
- package/dist/notifications-vectors.d.ts +427 -0
- package/dist/notifications-vectors.js +4949 -0
- package/dist/notifications.d.ts +265 -0
- package/dist/notifications.js +4958 -0
- package/dist/route-vectors.d.ts +23 -0
- package/dist/route-vectors.js +96 -0
- package/dist/routes.d.ts +60 -0
- package/dist/routes.js +209 -0
- package/dist/share-link-conformance.d.ts +47 -0
- package/dist/share-link-conformance.js +100 -0
- package/package.json +28 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PlatformCapabilities, PlatformGateway, PlatformTarget } from './index.js';
|
|
2
|
-
export declare const platformCapabilityKeys: readonly ["
|
|
3
|
-
export declare const optionalPlatformCapabilityKeys: readonly ["bannerAds"
|
|
2
|
+
export declare const platformCapabilityKeys: readonly ["nativeAds", "rewardedAds", "interstitialAds", "nativeLeaderboard", "remoteLeaderboard", "achievements", "cloudSave", "socialShare", "haptics", "localizedContent"];
|
|
3
|
+
export declare const optionalPlatformCapabilityKeys: readonly ["bannerAds"];
|
|
4
4
|
export interface PlatformGatewayCapabilityConformanceTransition {
|
|
5
5
|
readonly update: () => Promise<void> | void;
|
|
6
6
|
readonly expectedCapabilities: PlatformCapabilities;
|
|
@@ -24,10 +24,11 @@ export interface PlatformGatewayCapabilityConformanceReport {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Runs provider-neutral capability checks against real adapter gateways or
|
|
27
|
-
* target-configured wrappers. Every read must return
|
|
28
|
-
*
|
|
27
|
+
* target-configured wrappers. Every read must return every required boolean
|
|
28
|
+
* key, known readiness states, the commerce and notifications blocks under
|
|
29
|
+
* their own rules, a fresh object, and the provider's latest state.
|
|
29
30
|
* @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 #
|
|
31
|
+
* @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #12e3a3a Reviewed required booleans, the required commerce block, the optional banner default, ordered readiness comparison, and fresh nested rereads, including notification identity and dense purpose checks.
|
|
31
32
|
* @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
|
|
32
33
|
* @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
|
|
33
34
|
* @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { deriveLegacyCommerceCapabilities } from './commerce-capabilities.js';
|
|
2
1
|
export const platformCapabilityKeys = Object.freeze([
|
|
3
|
-
'nativeIap',
|
|
4
2
|
'nativeAds',
|
|
5
3
|
'rewardedAds',
|
|
6
4
|
'interstitialAds',
|
|
@@ -14,7 +12,6 @@ export const platformCapabilityKeys = Object.freeze([
|
|
|
14
12
|
]);
|
|
15
13
|
export const optionalPlatformCapabilityKeys = Object.freeze([
|
|
16
14
|
'bannerAds',
|
|
17
|
-
'subscriptionIap',
|
|
18
15
|
]);
|
|
19
16
|
const allowedPlatformCapabilityKeys = Object.freeze([
|
|
20
17
|
...platformCapabilityKeys,
|
|
@@ -24,11 +21,10 @@ const allowedPlatformCapabilityKeySet = new Set([
|
|
|
24
21
|
...allowedPlatformCapabilityKeys,
|
|
25
22
|
'providerAvailability',
|
|
26
23
|
'commerce',
|
|
24
|
+
'notifications',
|
|
27
25
|
]);
|
|
28
26
|
const optionalPlatformCapabilityKeySet = new Set(optionalPlatformCapabilityKeys);
|
|
29
27
|
const providerFeatureSet = new Set([
|
|
30
|
-
'nativeIap',
|
|
31
|
-
'subscriptionIap',
|
|
32
28
|
'rewardedAds',
|
|
33
29
|
'interstitialAds',
|
|
34
30
|
'bannerAds',
|
|
@@ -43,8 +39,8 @@ const providerAvailabilitySet = new Set([
|
|
|
43
39
|
'temporarily-unavailable',
|
|
44
40
|
'available',
|
|
45
41
|
]);
|
|
46
|
-
// The
|
|
47
|
-
//
|
|
42
|
+
// The commerce block follows the same rules as the boolean snapshot: exact
|
|
43
|
+
// keys, typed values, and a fresh object on every read.
|
|
48
44
|
const commerceCapabilityBlockKeys = Object.freeze({
|
|
49
45
|
purchase: ['consumable', 'durable', 'subscription', 'quantity'],
|
|
50
46
|
catalog: ['products', 'offers', 'localizedPrice'],
|
|
@@ -56,12 +52,38 @@ const commerceCapabilityEnums = Object.freeze({
|
|
|
56
52
|
'authority.evidence': ['client-token', 'host-callback', 'adapter-settled', 'server-push'],
|
|
57
53
|
'authority.accountBinding': ['required', 'optional', 'host'],
|
|
58
54
|
});
|
|
55
|
+
const notificationCapabilityFields = Object.freeze({
|
|
56
|
+
permission: {
|
|
57
|
+
scope: { kind: 'enum', values: ['app', 'topic'] },
|
|
58
|
+
prompt: { kind: 'enum', values: ['os', 'host', 'game'] },
|
|
59
|
+
readable: { kind: 'boolean' },
|
|
60
|
+
serverReadable: { kind: 'boolean' },
|
|
61
|
+
allowance: { kind: 'enum', values: ['persistent', 'per-send'] },
|
|
62
|
+
userGesture: { kind: 'boolean' },
|
|
63
|
+
promptLimit: { kind: 'count', optional: true },
|
|
64
|
+
maxTopicsPerPrompt: { kind: 'count', optional: true },
|
|
65
|
+
settings: { kind: 'boolean' },
|
|
66
|
+
},
|
|
67
|
+
remote: {
|
|
68
|
+
supported: { kind: 'boolean' },
|
|
69
|
+
purposes: { kind: 'purposes' },
|
|
70
|
+
addressing: { kind: 'enum', values: ['device', 'platform-user', 'none'] },
|
|
71
|
+
content: { kind: 'enum', values: ['free-form', 'host-template'] },
|
|
72
|
+
},
|
|
73
|
+
local: {
|
|
74
|
+
supported: { kind: 'boolean' },
|
|
75
|
+
maxPending: { kind: 'count', optional: true },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const notificationPurposes = ['service', 'requested', 'advertising'];
|
|
79
|
+
const notificationOpenPaths = ['payload', 'landing-url', 'host-link', 'none'];
|
|
59
80
|
/**
|
|
60
81
|
* Runs provider-neutral capability checks against real adapter gateways or
|
|
61
|
-
* target-configured wrappers. Every read must return
|
|
62
|
-
*
|
|
82
|
+
* target-configured wrappers. Every read must return every required boolean
|
|
83
|
+
* key, known readiness states, the commerce and notifications blocks under
|
|
84
|
+
* their own rules, a fresh object, and the provider's latest state.
|
|
63
85
|
* @evidence docs/specs/platform-capability-snapshots.md#snapshot-shape Checks each gateway read against the required boolean keys and expected provider state.
|
|
64
|
-
* @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #
|
|
86
|
+
* @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #12e3a3a Reviewed required booleans, the required commerce block, the optional banner default, ordered readiness comparison, and fresh nested rereads, including notification identity and dense purpose checks.
|
|
65
87
|
* @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
|
|
66
88
|
* @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
|
|
67
89
|
* @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
|
|
@@ -102,11 +124,12 @@ async function runFixture(fixture) {
|
|
|
102
124
|
: cloneExpectedCapabilities(fixture.transition.expectedCapabilities);
|
|
103
125
|
assertEqual(fixture.gateway.target, fixture.expectedTarget, 'gateway target must match the fixture target');
|
|
104
126
|
const first = await fixture.gateway.getCapabilities();
|
|
105
|
-
assertCapabilitySnapshot(first, initialExpected);
|
|
127
|
+
assertCapabilitySnapshot(first, initialExpected, fixture.gateway);
|
|
106
128
|
tryMutateSnapshot(first);
|
|
107
129
|
const second = await fixture.gateway.getCapabilities();
|
|
108
130
|
assert(second !== first, 'getCapabilities must return a fresh snapshot object for every read');
|
|
109
|
-
assertCapabilitySnapshot(second, initialExpected);
|
|
131
|
+
assertCapabilitySnapshot(second, initialExpected, fixture.gateway);
|
|
132
|
+
assertNotificationSnapshotFreshness(first, second);
|
|
110
133
|
if (fixture.transition === undefined || transitionExpected === undefined) {
|
|
111
134
|
return;
|
|
112
135
|
}
|
|
@@ -115,9 +138,23 @@ async function runFixture(fixture) {
|
|
|
115
138
|
const updated = await fixture.gateway.getCapabilities();
|
|
116
139
|
assert(updated !== second, 'getCapabilities must return a fresh snapshot after a provider transition');
|
|
117
140
|
assert(updated !== first, 'getCapabilities must not reuse an earlier snapshot after a provider transition');
|
|
118
|
-
assertCapabilitySnapshot(updated, transitionExpected);
|
|
141
|
+
assertCapabilitySnapshot(updated, transitionExpected, fixture.gateway);
|
|
142
|
+
assertNotificationSnapshotFreshness(first, updated);
|
|
143
|
+
assertNotificationSnapshotFreshness(second, updated);
|
|
144
|
+
}
|
|
145
|
+
function assertNotificationSnapshotFreshness(previous, next) {
|
|
146
|
+
const first = previous.notifications;
|
|
147
|
+
const second = next.notifications;
|
|
148
|
+
if (first === undefined || second === undefined) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
assert(second !== first, 'notification capabilities must return a fresh block for every read');
|
|
152
|
+
for (const key of ['permission', 'remote', 'local']) {
|
|
153
|
+
assert(second[key] !== first[key], `notification capabilities must return a fresh ${key} record`);
|
|
154
|
+
}
|
|
155
|
+
assert(second.remote.purposes !== first.remote.purposes, 'notification capabilities must return a fresh purposes list');
|
|
119
156
|
}
|
|
120
|
-
function assertCapabilitySnapshot(actual, expected) {
|
|
157
|
+
function assertCapabilitySnapshot(actual, expected, gateway) {
|
|
121
158
|
const actualKeys = Object.keys(actual).sort();
|
|
122
159
|
const missingKeys = platformCapabilityKeys.filter((key) => !Object.hasOwn(actual, key));
|
|
123
160
|
const unexpectedKeys = actualKeys.filter((key) => !allowedPlatformCapabilityKeySet.has(key));
|
|
@@ -132,14 +169,85 @@ function assertCapabilitySnapshot(actual, expected) {
|
|
|
132
169
|
assertProviderAvailabilityShape(actual.providerAvailability);
|
|
133
170
|
assertEqual(normalizedAvailability(actual.providerAvailability), normalizedAvailability(expected.providerAvailability), 'provider availability must match the expected provider state');
|
|
134
171
|
assertCommerceCapabilityBlock(actual, expected.commerce);
|
|
172
|
+
assertNotificationCapabilityBlock(actual, expected.notifications, gateway);
|
|
135
173
|
}
|
|
136
|
-
function
|
|
137
|
-
const actual = snapshot.
|
|
174
|
+
function assertNotificationCapabilityBlock(snapshot, expected, gateway) {
|
|
175
|
+
const actual = snapshot.notifications;
|
|
176
|
+
// The block describes the exposed provider (notifications RFC "Capabilities").
|
|
177
|
+
assertEqual(actual !== undefined, gateway.notificationsV2 !== undefined, 'notification capabilities must be present exactly when the gateway exposes notificationsV2');
|
|
138
178
|
if (actual === undefined) {
|
|
139
|
-
assert(expected === undefined, '
|
|
179
|
+
assert(expected === undefined, 'notification capabilities must be present when the fixture expects them');
|
|
140
180
|
return;
|
|
141
181
|
}
|
|
142
|
-
assert(expected !== undefined, '
|
|
182
|
+
assert(expected !== undefined, 'notification capabilities must be absent when the fixture omits them');
|
|
183
|
+
assert(isPlainRecord(actual), 'notification capabilities must be a non-array record');
|
|
184
|
+
assertEqual(JSON.stringify(Object.keys(actual).sort()), JSON.stringify(['availability', 'local', 'open', 'permission', 'remote']), 'notification capabilities must contain exactly availability, permission, remote, local, and open');
|
|
185
|
+
for (const [group, fields] of Object.entries(notificationCapabilityFields)) {
|
|
186
|
+
const nested = actual[group];
|
|
187
|
+
assert(isPlainRecord(nested), `notification ${group} capabilities must be a non-array record`);
|
|
188
|
+
const unknownKeys = Object.keys(nested).filter((key) => !Object.hasOwn(fields, key));
|
|
189
|
+
assertEqual(JSON.stringify(unknownKeys), '[]', `notification ${group} capabilities must contain no unknown keys`);
|
|
190
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
191
|
+
const path = `${group}.${key}`;
|
|
192
|
+
if (!Object.hasOwn(nested, key)) {
|
|
193
|
+
assert(field.optional === true, `notification capability ${path} is required`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
assertNotificationCapabilityField(path, field, nested[key]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
assert(providerAvailabilitySet.has(actual.availability), 'notification availability must be a known readiness state');
|
|
200
|
+
assert(notificationOpenPaths.includes(actual.open), `notification open must be one of ${notificationOpenPaths.join(', ')}`);
|
|
201
|
+
const block = actual;
|
|
202
|
+
assertEqual(block.remote.supported, block.remote.addressing !== 'none', "notification remote.addressing must be 'none' exactly when remote.supported is false");
|
|
203
|
+
assertEqual(normalizedNotifications(block), normalizedNotifications(expected), 'notification capabilities must match the expected provider state');
|
|
204
|
+
assertEqual(snapshot.providerAvailability?.pushNotifications, block.availability, 'providerAvailability.pushNotifications must equal the notification capabilities availability');
|
|
205
|
+
}
|
|
206
|
+
function assertNotificationCapabilityField(path, field, value) {
|
|
207
|
+
switch (field.kind) {
|
|
208
|
+
case 'boolean':
|
|
209
|
+
assertEqual(typeof value, 'boolean', `notification capability ${path} must be a boolean`);
|
|
210
|
+
return;
|
|
211
|
+
case 'enum':
|
|
212
|
+
assert(field.values.includes(value), `notification capability ${path} must be one of ${field.values.join(', ')}`);
|
|
213
|
+
return;
|
|
214
|
+
case 'count':
|
|
215
|
+
assert(Number.isInteger(value) && value >= 1 && value <= 0xffff_ffff, `notification capability ${path} must be a positive integer`);
|
|
216
|
+
return;
|
|
217
|
+
case 'purposes':
|
|
218
|
+
assert(isDenseNotificationPurposeList(value), `notification capability ${path} must list only ${notificationPurposes.join(', ')}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function isDenseNotificationPurposeList(value) {
|
|
223
|
+
if (!Array.isArray(value)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
227
|
+
if (!Object.hasOwn(value, index) || !notificationPurposes.includes(value[index])) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
function normalizedNotifications(value) {
|
|
234
|
+
if (value === undefined) {
|
|
235
|
+
return 'undefined';
|
|
236
|
+
}
|
|
237
|
+
return JSON.stringify([
|
|
238
|
+
['availability', value.availability],
|
|
239
|
+
['permission', sortedEntries(value.permission)],
|
|
240
|
+
['remote', sortedEntries({ ...value.remote, purposes: [...value.remote.purposes].sort() })],
|
|
241
|
+
['local', sortedEntries(value.local)],
|
|
242
|
+
['open', value.open],
|
|
243
|
+
]);
|
|
244
|
+
}
|
|
245
|
+
function isPlainRecord(value) {
|
|
246
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
247
|
+
}
|
|
248
|
+
function assertCommerceCapabilityBlock(snapshot, expected) {
|
|
249
|
+
const actual = snapshot.commerce;
|
|
250
|
+
assert(actual !== undefined, 'commerce capabilities are required: a gateway without a commerce provider reports createUnsupportedCommerceCapabilities()');
|
|
143
251
|
assert(typeof actual === 'object' && actual !== null && !Array.isArray(actual), 'commerce capabilities must be a non-array record');
|
|
144
252
|
const block = actual;
|
|
145
253
|
const blockKeys = Object.keys(block).sort();
|
|
@@ -162,9 +270,6 @@ function assertCommerceCapabilityBlock(snapshot, expected) {
|
|
|
162
270
|
}
|
|
163
271
|
assert(providerAvailabilitySet.has(block.availability), 'commerce availability must be a known readiness state');
|
|
164
272
|
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
273
|
}
|
|
169
274
|
function normalizedCommerce(value) {
|
|
170
275
|
if (value === undefined) {
|
|
@@ -191,41 +296,37 @@ function assertProviderAvailabilityShape(value) {
|
|
|
191
296
|
&& providerAvailabilitySet.has(state), 'provider availability must contain only known features and readiness states');
|
|
192
297
|
}
|
|
193
298
|
}
|
|
299
|
+
// A deep copy of the plain records and lists, whatever their shape: the
|
|
300
|
+
// oracle stays independent of the provider even when a fixture expects a
|
|
301
|
+
// malformed block to be reported as is.
|
|
194
302
|
function cloneExpectedCapabilities(value) {
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
catalog: { ...value.commerce.catalog },
|
|
206
|
-
lifecycle: { ...value.commerce.lifecycle },
|
|
207
|
-
authority: { ...value.commerce.authority },
|
|
208
|
-
availability: value.commerce.availability,
|
|
209
|
-
},
|
|
210
|
-
}),
|
|
211
|
-
};
|
|
303
|
+
return clonePlainData(value);
|
|
304
|
+
}
|
|
305
|
+
function clonePlainData(value) {
|
|
306
|
+
if (Array.isArray(value)) {
|
|
307
|
+
return value.map((entry) => clonePlainData(entry));
|
|
308
|
+
}
|
|
309
|
+
if (typeof value === 'object' && value !== null) {
|
|
310
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clonePlainData(entry)]));
|
|
311
|
+
}
|
|
312
|
+
return value;
|
|
212
313
|
}
|
|
213
314
|
function normalizedAvailability(value) {
|
|
214
315
|
return JSON.stringify(Object.entries(value ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
215
316
|
}
|
|
216
317
|
function tryMutateSnapshot(snapshot) {
|
|
217
318
|
try {
|
|
218
|
-
Reflect.set(snapshot, '
|
|
319
|
+
Reflect.set(snapshot, 'nativeAds', !snapshot.nativeAds);
|
|
219
320
|
}
|
|
220
321
|
catch {
|
|
221
322
|
// Frozen snapshots already satisfy the isolation requirement.
|
|
222
323
|
}
|
|
223
324
|
if (snapshot.providerAvailability !== undefined) {
|
|
224
325
|
try {
|
|
225
|
-
const next = snapshot.providerAvailability.
|
|
326
|
+
const next = snapshot.providerAvailability.rewardedAds === 'available'
|
|
226
327
|
? 'unsupported'
|
|
227
328
|
: 'available';
|
|
228
|
-
Reflect.set(snapshot.providerAvailability, '
|
|
329
|
+
Reflect.set(snapshot.providerAvailability, 'rewardedAds', next);
|
|
229
330
|
}
|
|
230
331
|
catch {
|
|
231
332
|
// A frozen nested readiness record already satisfies isolation.
|
|
@@ -234,6 +335,39 @@ function tryMutateSnapshot(snapshot) {
|
|
|
234
335
|
if (snapshot.commerce !== undefined) {
|
|
235
336
|
tryMutateCommerceSnapshot(snapshot.commerce);
|
|
236
337
|
}
|
|
338
|
+
if (snapshot.notifications !== undefined) {
|
|
339
|
+
tryMutateNotificationSnapshot(snapshot.notifications);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Every nested record and the purposes list are probed, as for commerce.
|
|
343
|
+
function tryMutateNotificationSnapshot(notifications) {
|
|
344
|
+
const { purposes } = notifications.remote;
|
|
345
|
+
const probes = [
|
|
346
|
+
[notifications.permission, 'readable', !notifications.permission.readable],
|
|
347
|
+
[
|
|
348
|
+
notifications.remote,
|
|
349
|
+
'content',
|
|
350
|
+
notifications.remote.content === 'free-form' ? 'host-template' : 'free-form',
|
|
351
|
+
],
|
|
352
|
+
// A reused list shows on the next read: emptied when it lists purposes,
|
|
353
|
+
// or holding one when it was empty.
|
|
354
|
+
purposes.length === 0 ? [purposes, '0', 'service'] : [purposes, 'length', 0],
|
|
355
|
+
[notifications.local, 'supported', !notifications.local.supported],
|
|
356
|
+
[
|
|
357
|
+
notifications,
|
|
358
|
+
'availability',
|
|
359
|
+
notifications.availability === 'available' ? 'unsupported' : 'available',
|
|
360
|
+
],
|
|
361
|
+
[notifications, 'open', notifications.open === 'none' ? 'payload' : 'none'],
|
|
362
|
+
];
|
|
363
|
+
for (const [target, key, next] of probes) {
|
|
364
|
+
try {
|
|
365
|
+
Reflect.set(target, key, next);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Frozen values still need distinct references, checked across snapshots.
|
|
369
|
+
}
|
|
370
|
+
}
|
|
237
371
|
}
|
|
238
372
|
// Every nested block is probed: a gateway may rebuild the outer object while
|
|
239
373
|
// reusing one mutable nested record across reads.
|
|
@@ -263,7 +397,9 @@ function hasCapabilityDifference(first, second) {
|
|
|
263
397
|
.some((key) => (first[key] ?? false) !== (second[key] ?? false))
|
|
264
398
|
|| normalizedAvailability(first.providerAvailability)
|
|
265
399
|
!== normalizedAvailability(second.providerAvailability)
|
|
266
|
-
|| normalizedCommerce(first.commerce) !== normalizedCommerce(second.commerce)
|
|
400
|
+
|| normalizedCommerce(first.commerce) !== normalizedCommerce(second.commerce)
|
|
401
|
+
|| normalizedNotifications(first.notifications)
|
|
402
|
+
!== normalizedNotifications(second.notifications);
|
|
267
403
|
}
|
|
268
404
|
function assert(condition, message) {
|
|
269
405
|
if (!condition) {
|
|
@@ -1,9 +1,3 @@
|
|
|
1
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
2
|
/** A fresh all-false snapshot; callers must not share or mutate a returned object. */
|
|
9
3
|
export declare function createUnsupportedCommerceCapabilities(): CommerceCapabilities;
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return {
|
|
4
|
-
nativeIap: capabilities.availability === 'available'
|
|
5
|
-
&& (capabilities.purchase.consumable || capabilities.purchase.durable),
|
|
6
|
-
subscriptionIap: capabilities.purchase.subscription,
|
|
7
|
-
};
|
|
8
|
-
}
|
|
1
|
+
// Pure capability helpers shared by `./commerce` and `./capability-conformance`.
|
|
2
|
+
// This file imports no validator so the conformance runner stays free of typia.
|
|
9
3
|
/** A fresh all-false snapshot; callers must not share or mutate a returned object. */
|
|
10
4
|
export function createUnsupportedCommerceCapabilities() {
|
|
11
5
|
return {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { CommerceCapabilities, CommerceFinishOwner, LogicalProductId, PlatformEvidenceEnvelope, StarterLogicalProductId } from './index.js';
|
|
1
|
+
import type { CommerceCapabilities, CommerceFinishOwner, LogicalProductId, PlatformEvidenceEnvelope, PlatformPurchasePresentationEvent, StarterLogicalProductId } from './index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Commerce v2 client contract types (RFC "Client contract"). This module is
|
|
4
4
|
* typia-free on purpose: `./index` references `CommerceProvider` for
|
|
5
|
-
* `PlatformGateway.
|
|
5
|
+
* `PlatformGateway.commerce`, and headless consumers type-check
|
|
6
6
|
* `@mpgd/platform` with `skipLibCheck: false` and no DOM lib, where typia's
|
|
7
7
|
* root declarations do not compile. `./commerce` re-exports everything here
|
|
8
8
|
* and builds the validators and JSON Schemas from these same types.
|
|
@@ -193,6 +193,16 @@ export interface FinishTransactionResult {
|
|
|
193
193
|
}
|
|
194
194
|
export interface CommerceProvider {
|
|
195
195
|
readonly store: StoreId;
|
|
196
|
+
/**
|
|
197
|
+
* Trusted native presentation facts for the purchase UI (opened, closed,
|
|
198
|
+
* never started), independent of the purchase result. A provider whose host
|
|
199
|
+
* can prove when its payment surface closes (ONE play) offers it; the game
|
|
200
|
+
* runtime's presentation gate subscribes when present and treats a result
|
|
201
|
+
* alone as unknown.
|
|
202
|
+
*/
|
|
203
|
+
readonly presentation?: {
|
|
204
|
+
subscribe(listener: (event: PlatformPurchasePresentationEvent) => void): () => void;
|
|
205
|
+
};
|
|
196
206
|
getCapabilities(): Promise<CommerceCapabilities>;
|
|
197
207
|
fetchProducts(input: FetchProductsInput): Promise<readonly StoreProduct[]>;
|
|
198
208
|
/** Returns `launched` when the result arrives later as a `purchaseUpdated` event. */
|
package/dist/commerce.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type tags } from 'typia';
|
|
2
|
-
import { PlatformOperationError, type
|
|
3
|
-
import type { ActiveSubscription, CommerceErrorCode, CommerceErrorInfo, CommerceEvent, CommerceLogicalProductId,
|
|
4
|
-
export { createUnsupportedCommerceCapabilities
|
|
2
|
+
import { PlatformOperationError, type CommerceCapabilities } from './index.js';
|
|
3
|
+
import type { ActiveSubscription, CommerceErrorCode, CommerceErrorInfo, CommerceEvent, CommerceLogicalProductId, CommerceServiceCapabilities, FetchProductsInput, FinishTransactionInput, FinishTransactionResult, GetAvailablePurchasesInput, IsoTimestamp, NonEmptyString, NonNegativeInteger, PositiveQuantity, PurchaseKind, RequestPurchaseInput, RequestPurchaseResult, StoreId, StoreProduct, StorePurchase } from './commerce-contract.js';
|
|
4
|
+
export { createUnsupportedCommerceCapabilities } from './commerce-capabilities.js';
|
|
5
5
|
export type { CommerceAccountBindingPolicy, CommerceAuthorityCapabilities, CommerceCapabilities, CommerceCatalogCapabilities, CommerceEvidenceAuthority, CommerceFinishOwner, CommerceLifecycleCapabilities, CommercePurchaseCapabilities, } from './index.js';
|
|
6
6
|
/**
|
|
7
7
|
* Commerce v2 client contract. Pure types, validators, and helpers only: no
|
|
@@ -255,7 +255,11 @@ export interface DerivedPurchaseIdempotencyKeyInput {
|
|
|
255
255
|
* byte-identical.
|
|
256
256
|
*/
|
|
257
257
|
export declare function createDerivedPurchaseIdempotencyKey(input: DerivedPurchaseIdempotencyKeyInput): Promise<string>;
|
|
258
|
-
/**
|
|
258
|
+
/**
|
|
259
|
+
* The `play-token-sha256:<hex>` idempotency key of a Google Play purchase
|
|
260
|
+
* token. The one shared helper: the plugin and `@mpgd/game-services` use it,
|
|
261
|
+
* and ledgers written by v1 releases keep deduplicating against it.
|
|
262
|
+
*/
|
|
259
263
|
export declare function createGooglePlayTokenPurchaseKey(purchaseToken: string): Promise<string>;
|
|
260
264
|
export declare const commerceEventBufferLimit = 64;
|
|
261
265
|
export interface CommerceEventBuffer {
|
|
@@ -317,16 +321,3 @@ export declare const assertCommerceBridgeFinishTransactionPayload: (input: unkno
|
|
|
317
321
|
export declare const assertCommerceBridgeFinishTransactionResult: (input: unknown) => FinishTransactionResult;
|
|
318
322
|
export declare const assertCommerceBridgeRestorePurchasesPayload: (input: unknown) => CommerceBridgeRestorePurchasesPayload;
|
|
319
323
|
export declare const assertCommerceBridgeRestorePurchasesResult: (input: unknown) => CommerceBridgeRestorePurchasesResult;
|
|
320
|
-
export interface ToCommerceAdapterOptions {
|
|
321
|
-
/** Forwarded to `getAvailablePurchases` during `restore`. Defaults to false. */
|
|
322
|
-
readonly includePending?: boolean;
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Serves v1 `CommerceAdapter` callers from a v2 provider. The wrapper never
|
|
326
|
-
* grants: a `purchased` outcome becomes a `completed` result with no
|
|
327
|
-
* entitlements, and `authoritativeGrant` appears only when the provider reports
|
|
328
|
-
* an adapter settlement that already points at a ledger entry. `restore`
|
|
329
|
-
* returns settled purchases from `getAvailablePurchases` and no entitlements;
|
|
330
|
-
* entitlements come from the ledger, not from the provider.
|
|
331
|
-
*/
|
|
332
|
-
export declare function toCommerceAdapter(provider: CommerceProvider, options?: ToCommerceAdapterOptions): CommerceAdapter;
|
package/dist/commerce.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as _accessExpressionAsString_1 from "typia/lib/internal/_accessExpressi
|
|
|
6
6
|
const __typia_transform__accessExpressionAsString = _accessExpressionAsString_1._accessExpressionAsString;
|
|
7
7
|
import typia from 'typia';
|
|
8
8
|
import { PlatformOperationError, readPlatformOperationFailure, } from './index.js';
|
|
9
|
-
export { createUnsupportedCommerceCapabilities
|
|
9
|
+
export { createUnsupportedCommerceCapabilities } from './commerce-capabilities.js';
|
|
10
10
|
/**
|
|
11
11
|
* Commerce v2 client contract. Pure types, validators, and helpers only: no
|
|
12
12
|
* provider lives here, and nothing in this module grants a purchase. The
|
|
@@ -39,6 +39,10 @@ export const commerceBridgeMethods = Object.freeze([
|
|
|
39
39
|
'commerce.v2.finishTransaction',
|
|
40
40
|
'commerce.v2.restorePurchases',
|
|
41
41
|
]);
|
|
42
|
+
// `satisfies` only rejects an unknown name; this rejects a missing one, so a
|
|
43
|
+
// method added to `CommerceBridgeMethods` cannot be left out of the list.
|
|
44
|
+
const commerceBridgeMethodListIsExhaustive = true;
|
|
45
|
+
void commerceBridgeMethodListIsExhaustive;
|
|
42
46
|
// Errors.
|
|
43
47
|
const retryableCommerceErrorCodes = new Set([
|
|
44
48
|
'NETWORK',
|
|
@@ -226,7 +230,11 @@ export async function createDerivedPurchaseIdempotencyKey(input) {
|
|
|
226
230
|
}
|
|
227
231
|
return `store:${encodeURIComponent(store)}:${encodeURIComponent(transactionId)}`;
|
|
228
232
|
}
|
|
229
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* The `play-token-sha256:<hex>` idempotency key of a Google Play purchase
|
|
235
|
+
* token. The one shared helper: the plugin and `@mpgd/game-services` use it,
|
|
236
|
+
* and ledgers written by v1 releases keep deduplicating against it.
|
|
237
|
+
*/
|
|
230
238
|
export async function createGooglePlayTokenPurchaseKey(purchaseToken) {
|
|
231
239
|
if (readIdentifier(purchaseToken, 4096) === undefined) {
|
|
232
240
|
throw new TypeError('Google Play purchase token is invalid.');
|
|
@@ -5757,95 +5765,3 @@ export const assertCommerceBridgeRestorePurchasesResult = (() => {
|
|
|
5757
5765
|
return input;
|
|
5758
5766
|
};
|
|
5759
5767
|
})();
|
|
5760
|
-
const purchaseKindToProductType = Object.freeze({
|
|
5761
|
-
consumable: 'consumable',
|
|
5762
|
-
durable: 'non_consumable',
|
|
5763
|
-
subscription: 'subscription',
|
|
5764
|
-
});
|
|
5765
|
-
/**
|
|
5766
|
-
* Serves v1 `CommerceAdapter` callers from a v2 provider. The wrapper never
|
|
5767
|
-
* grants: a `purchased` outcome becomes a `completed` result with no
|
|
5768
|
-
* entitlements, and `authoritativeGrant` appears only when the provider reports
|
|
5769
|
-
* an adapter settlement that already points at a ledger entry. `restore`
|
|
5770
|
-
* returns settled purchases from `getAvailablePurchases` and no entitlements;
|
|
5771
|
-
* entitlements come from the ledger, not from the provider.
|
|
5772
|
-
*/
|
|
5773
|
-
export function toCommerceAdapter(provider, options = {}) {
|
|
5774
|
-
const includePending = options.includePending ?? false;
|
|
5775
|
-
return {
|
|
5776
|
-
async getProducts() {
|
|
5777
|
-
const products = await provider.fetchProducts({ type: 'all' });
|
|
5778
|
-
return products.map((product) => ({
|
|
5779
|
-
id: product.logicalId,
|
|
5780
|
-
type: purchaseKindToProductType[product.kind],
|
|
5781
|
-
title: product.title,
|
|
5782
|
-
description: product.description,
|
|
5783
|
-
price: { formatted: product.price.formatted, currencyCode: product.price.currencyCode },
|
|
5784
|
-
}));
|
|
5785
|
-
},
|
|
5786
|
-
async purchase(input) {
|
|
5787
|
-
const result = await provider.requestPurchase({
|
|
5788
|
-
logicalId: input.productId,
|
|
5789
|
-
idempotencyKey: input.idempotencyKey,
|
|
5790
|
-
source: input.source,
|
|
5791
|
-
});
|
|
5792
|
-
return toPurchaseResult(result);
|
|
5793
|
-
},
|
|
5794
|
-
async restore() {
|
|
5795
|
-
await provider.restorePurchases?.();
|
|
5796
|
-
const purchases = await provider.getAvailablePurchases({ includePending });
|
|
5797
|
-
const settledPurchases = [];
|
|
5798
|
-
for (const purchase of purchases) {
|
|
5799
|
-
// A pending purchase is never a settled outcome, even when the adapter
|
|
5800
|
-
// already attached a ledger pointer to it.
|
|
5801
|
-
if (purchase.settlement === undefined
|
|
5802
|
-
|| purchase.logicalId === undefined
|
|
5803
|
-
|| purchase.state === 'pending') {
|
|
5804
|
-
continue;
|
|
5805
|
-
}
|
|
5806
|
-
settledPurchases.push({
|
|
5807
|
-
transactionId: purchase.transactionId,
|
|
5808
|
-
productId: purchase.logicalId,
|
|
5809
|
-
status: purchase.state === 'revoked' ? 'refunded' : 'granted',
|
|
5810
|
-
ledgerEntryId: purchase.settlement.ledgerEntryId,
|
|
5811
|
-
});
|
|
5812
|
-
}
|
|
5813
|
-
return { restoredEntitlements: [], settledPurchases };
|
|
5814
|
-
},
|
|
5815
|
-
async getEntitlements() {
|
|
5816
|
-
return [];
|
|
5817
|
-
},
|
|
5818
|
-
};
|
|
5819
|
-
}
|
|
5820
|
-
function toPurchaseResult(result) {
|
|
5821
|
-
if (result.outcome === 'launched') {
|
|
5822
|
-
return { status: 'pending', entitlementIds: [] };
|
|
5823
|
-
}
|
|
5824
|
-
if (result.outcome === 'purchased') {
|
|
5825
|
-
const { purchase } = result;
|
|
5826
|
-
return {
|
|
5827
|
-
status: purchase.state === 'purchased' ? 'completed' : 'pending',
|
|
5828
|
-
transactionId: purchase.transactionId,
|
|
5829
|
-
entitlementIds: [],
|
|
5830
|
-
evidence: purchase.evidence,
|
|
5831
|
-
...(purchase.settlement === undefined || purchase.state !== 'purchased'
|
|
5832
|
-
? {}
|
|
5833
|
-
: { authoritativeGrant: { ledgerEntryId: purchase.settlement.ledgerEntryId } }),
|
|
5834
|
-
};
|
|
5835
|
-
}
|
|
5836
|
-
return {
|
|
5837
|
-
status: result.outcome,
|
|
5838
|
-
entitlementIds: [],
|
|
5839
|
-
diagnostic: toCommerceDiagnostic(result.error),
|
|
5840
|
-
};
|
|
5841
|
-
}
|
|
5842
|
-
function toCommerceDiagnostic(error) {
|
|
5843
|
-
const providerCode = error.storeCode;
|
|
5844
|
-
return {
|
|
5845
|
-
code: error.code,
|
|
5846
|
-
retryable: error.retryable,
|
|
5847
|
-
...(providerCode !== undefined && /^[A-Za-z0-9_.:-]{1,128}$/u.test(providerCode)
|
|
5848
|
-
? { providerCode }
|
|
5849
|
-
: {}),
|
|
5850
|
-
};
|
|
5851
|
-
}
|