@mpgd/platform 0.12.1 → 0.13.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/capability-conformance.d.ts +7 -1
- package/dist/capability-conformance.js +77 -7
- package/dist/index.d.ts +119 -0
- package/dist/index.js +35 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -26,5 +26,11 @@ export interface PlatformGatewayCapabilityConformanceReport {
|
|
|
26
26
|
* Runs provider-neutral capability checks against real adapter gateways or
|
|
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
|
+
* @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 #a1834db Reviewed required booleans, optional banner/subscription defaults, ordered readiness comparison, and fresh nested rereads.
|
|
31
|
+
* @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
|
|
32
|
+
* @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
|
|
33
|
+
* @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
|
|
34
|
+
* @evidenceReview docs/specs/platform-capability-snapshots.md#fixture-validation #d566052 Checked the nonempty and unique name guards and target assertion.
|
|
29
35
|
*/
|
|
30
36
|
export declare function runPlatformGatewayCapabilityConformance(input: RunPlatformGatewayCapabilityConformanceInput): Promise<PlatformGatewayCapabilityConformanceReport>;
|
|
@@ -13,17 +13,44 @@ 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(
|
|
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.
|
|
48
|
+
* @evidence docs/specs/platform-capability-snapshots.md#snapshot-shape Checks each gateway read against the required boolean keys and expected provider state.
|
|
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.
|
|
50
|
+
* @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
|
|
51
|
+
* @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
|
|
52
|
+
* @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
|
|
53
|
+
* @evidenceReview docs/specs/platform-capability-snapshots.md#fixture-validation #d566052 Checked the nonempty and unique name guards and target assertion.
|
|
27
54
|
*/
|
|
28
55
|
export async function runPlatformGatewayCapabilityConformance(input) {
|
|
29
56
|
if (input.fixtures.length === 0) {
|
|
@@ -52,21 +79,28 @@ export async function runPlatformGatewayCapabilityConformance(input) {
|
|
|
52
79
|
return { passedFixtures };
|
|
53
80
|
}
|
|
54
81
|
async function runFixture(fixture) {
|
|
82
|
+
// A fixture may accidentally share its expected object with the provider.
|
|
83
|
+
// Keep the oracle independent before tryMutateSnapshot probes isolation.
|
|
84
|
+
const initialExpected = cloneExpectedCapabilities(fixture.expectedCapabilities);
|
|
85
|
+
const transitionExpected = fixture.transition === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: cloneExpectedCapabilities(fixture.transition.expectedCapabilities);
|
|
55
88
|
assertEqual(fixture.gateway.target, fixture.expectedTarget, 'gateway target must match the fixture target');
|
|
56
89
|
const first = await fixture.gateway.getCapabilities();
|
|
57
|
-
assertCapabilitySnapshot(first,
|
|
90
|
+
assertCapabilitySnapshot(first, initialExpected);
|
|
58
91
|
tryMutateSnapshot(first);
|
|
59
92
|
const second = await fixture.gateway.getCapabilities();
|
|
60
93
|
assert(second !== first, 'getCapabilities must return a fresh snapshot object for every read');
|
|
61
|
-
assertCapabilitySnapshot(second,
|
|
62
|
-
if (fixture.transition === undefined) {
|
|
94
|
+
assertCapabilitySnapshot(second, initialExpected);
|
|
95
|
+
if (fixture.transition === undefined || transitionExpected === undefined) {
|
|
63
96
|
return;
|
|
64
97
|
}
|
|
65
|
-
assert(hasCapabilityDifference(
|
|
98
|
+
assert(hasCapabilityDifference(initialExpected, transitionExpected), 'a transition fixture must change at least one capability');
|
|
66
99
|
await fixture.transition.update();
|
|
67
100
|
const updated = await fixture.gateway.getCapabilities();
|
|
68
101
|
assert(updated !== second, 'getCapabilities must return a fresh snapshot after a provider transition');
|
|
69
|
-
|
|
102
|
+
assert(updated !== first, 'getCapabilities must not reuse an earlier snapshot after a provider transition');
|
|
103
|
+
assertCapabilitySnapshot(updated, transitionExpected);
|
|
70
104
|
}
|
|
71
105
|
function assertCapabilitySnapshot(actual, expected) {
|
|
72
106
|
const actualKeys = Object.keys(actual).sort();
|
|
@@ -80,6 +114,29 @@ function assertCapabilitySnapshot(actual, expected) {
|
|
|
80
114
|
}
|
|
81
115
|
assertEqual(optional ? (actual[key] ?? false) : actual[key], optional ? (expected[key] ?? false) : expected[key], `capability ${key} must match the expected provider state`);
|
|
82
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)));
|
|
83
140
|
}
|
|
84
141
|
function tryMutateSnapshot(snapshot) {
|
|
85
142
|
try {
|
|
@@ -88,10 +145,23 @@ function tryMutateSnapshot(snapshot) {
|
|
|
88
145
|
catch {
|
|
89
146
|
// Frozen snapshots already satisfy the isolation requirement.
|
|
90
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
|
+
}
|
|
91
159
|
}
|
|
92
160
|
function hasCapabilityDifference(first, second) {
|
|
93
161
|
return allowedPlatformCapabilityKeys
|
|
94
|
-
.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);
|
|
95
165
|
}
|
|
96
166
|
function assert(condition, message) {
|
|
97
167
|
if (!condition) {
|
package/dist/index.d.ts
CHANGED
|
@@ -59,7 +59,29 @@ export interface PurchaseResult {
|
|
|
59
59
|
}
|
|
60
60
|
export interface PurchaseRestoreResult {
|
|
61
61
|
readonly restoredEntitlements: readonly Entitlement[];
|
|
62
|
+
/** Server-confirmed consumable outcomes; native order visibility alone is not a grant. */
|
|
63
|
+
readonly settledPurchases?: readonly PurchaseSettlement[];
|
|
62
64
|
}
|
|
65
|
+
export interface PurchaseSettlement {
|
|
66
|
+
readonly transactionId: string;
|
|
67
|
+
readonly productId: LogicalProductId;
|
|
68
|
+
readonly status: 'granted' | 'refunded';
|
|
69
|
+
readonly ledgerEntryId?: string;
|
|
70
|
+
readonly alreadyProcessed?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a specific purchase from authoritative checkout or restore evidence.
|
|
74
|
+
* A completed native checkout without authoritativeGrant is still unconfirmed.
|
|
75
|
+
* This is UI status evidence, never permission to credit a wallet locally.
|
|
76
|
+
* The caller must pair `purchase` with its requested `productId` because
|
|
77
|
+
* PurchaseResult itself does not carry a product identifier.
|
|
78
|
+
*/
|
|
79
|
+
export declare function findAuthoritativePurchaseSettlement(input: {
|
|
80
|
+
readonly productId: LogicalProductId;
|
|
81
|
+
readonly transactionId?: string;
|
|
82
|
+
readonly purchase?: PurchaseResult;
|
|
83
|
+
readonly restore?: PurchaseRestoreResult;
|
|
84
|
+
}): PurchaseSettlement | null;
|
|
63
85
|
export interface RewardedAdResult {
|
|
64
86
|
readonly status: 'completed' | 'skipped' | 'unavailable' | 'failed';
|
|
65
87
|
readonly rewardGranted: boolean;
|
|
@@ -92,6 +114,8 @@ export interface CommerceAdapter {
|
|
|
92
114
|
export interface AdAdapter {
|
|
93
115
|
preload(input: {
|
|
94
116
|
readonly placementId: LogicalAdPlacementId;
|
|
117
|
+
/** Allows a shared provider preload method to enforce format readiness. */
|
|
118
|
+
readonly format?: 'rewarded' | 'interstitial' | 'banner';
|
|
95
119
|
}): Promise<void>;
|
|
96
120
|
showRewarded(input: {
|
|
97
121
|
readonly placementId: LogicalAdPlacementId;
|
|
@@ -139,6 +163,8 @@ export interface LeaderboardScoreInput {
|
|
|
139
163
|
readonly score: number;
|
|
140
164
|
readonly runId: string;
|
|
141
165
|
readonly submittedAt: string;
|
|
166
|
+
/** Optional target-selected route when native and remote backends coexist. */
|
|
167
|
+
readonly route?: 'native' | 'remote';
|
|
142
168
|
}
|
|
143
169
|
export interface LeaderboardSubmitResult {
|
|
144
170
|
readonly submitted: boolean;
|
|
@@ -148,10 +174,17 @@ export interface LeaderboardAdapter {
|
|
|
148
174
|
submitScore(input: LeaderboardScoreInput): Promise<LeaderboardSubmitResult>;
|
|
149
175
|
open(input?: {
|
|
150
176
|
readonly leaderboardId?: string;
|
|
177
|
+
readonly route?: 'native' | 'remote';
|
|
151
178
|
}): Promise<void>;
|
|
152
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';
|
|
153
184
|
export interface PlatformCapabilities {
|
|
154
185
|
readonly nativeIap: boolean;
|
|
186
|
+
/** Optional for adapters published before subscription-specific support. */
|
|
187
|
+
readonly subscriptionIap?: boolean;
|
|
155
188
|
readonly nativeAds: boolean;
|
|
156
189
|
readonly rewardedAds: boolean;
|
|
157
190
|
readonly interstitialAds: boolean;
|
|
@@ -165,6 +198,8 @@ export interface PlatformCapabilities {
|
|
|
165
198
|
readonly socialShare: boolean;
|
|
166
199
|
readonly haptics: boolean;
|
|
167
200
|
readonly localizedContent: boolean;
|
|
201
|
+
/** Explains a false capability without claiming an unavailable SDK works. */
|
|
202
|
+
readonly providerAvailability?: Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>;
|
|
168
203
|
}
|
|
169
204
|
export interface PlayerIdentity {
|
|
170
205
|
readonly playerId: string;
|
|
@@ -245,9 +280,77 @@ export interface NotificationSubscriptionAdapter {
|
|
|
245
280
|
getStatus(topic: NotificationTopic): Promise<NotificationSubscriptionStatus>;
|
|
246
281
|
requestSubscription(topic: NotificationTopic): Promise<NotificationSubscriptionResult>;
|
|
247
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
|
+
}
|
|
248
330
|
export interface LifecycleAdapter {
|
|
249
331
|
onPause(callback: () => void): () => void;
|
|
250
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>;
|
|
251
354
|
}
|
|
252
355
|
export interface StorageLoadResult {
|
|
253
356
|
readonly value: unknown;
|
|
@@ -262,6 +365,19 @@ export interface StorageAdapter {
|
|
|
262
365
|
readonly value: unknown;
|
|
263
366
|
}): Promise<void>;
|
|
264
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
|
+
}
|
|
265
381
|
export interface PlatformGateway {
|
|
266
382
|
readonly target: PlatformTarget;
|
|
267
383
|
getCapabilities(): Promise<PlatformCapabilities>;
|
|
@@ -270,7 +386,10 @@ export interface PlatformGateway {
|
|
|
270
386
|
readonly ads: AdAdapter;
|
|
271
387
|
readonly leaderboard: LeaderboardAdapter;
|
|
272
388
|
readonly lifecycle: LifecycleAdapter;
|
|
389
|
+
readonly viewport?: ViewportAdapter;
|
|
273
390
|
readonly storage: StorageAdapter;
|
|
391
|
+
/** Optional: missing native support must not fall back to plaintext storage. */
|
|
392
|
+
readonly secureCredentials?: SecureCredentialStore;
|
|
274
393
|
readonly presentation?: PresentationAdapter;
|
|
275
394
|
readonly sharing?: ShareAdapter;
|
|
276
395
|
readonly notifications?: NotificationSubscriptionAdapter;
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,41 @@ function isPlatformOperationCode(code) {
|
|
|
41
41
|
&& code.length <= 128
|
|
42
42
|
&& /^[A-Z][A-Z0-9_:-]*$/u.test(code);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a specific purchase from authoritative checkout or restore evidence.
|
|
46
|
+
* A completed native checkout without authoritativeGrant is still unconfirmed.
|
|
47
|
+
* This is UI status evidence, never permission to credit a wallet locally.
|
|
48
|
+
* The caller must pair `purchase` with its requested `productId` because
|
|
49
|
+
* PurchaseResult itself does not carry a product identifier.
|
|
50
|
+
*/
|
|
51
|
+
export function findAuthoritativePurchaseSettlement(input) {
|
|
52
|
+
const effectiveTransactionId = input.transactionId ?? input.purchase?.transactionId;
|
|
53
|
+
const matchingRestored = input.restore?.settledPurchases?.filter((settlement) => settlement.productId === input.productId
|
|
54
|
+
&& (effectiveTransactionId === undefined || settlement.transactionId === effectiveTransactionId));
|
|
55
|
+
// Without an order ID, multiple historical outcomes are ambiguous.
|
|
56
|
+
if (matchingRestored !== undefined && matchingRestored.length > 1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (matchingRestored?.[0] !== undefined) {
|
|
60
|
+
return matchingRestored[0];
|
|
61
|
+
}
|
|
62
|
+
const purchase = input.purchase;
|
|
63
|
+
if (purchase?.status !== 'completed'
|
|
64
|
+
|| purchase.transactionId === undefined
|
|
65
|
+
|| purchase.authoritativeGrant === undefined
|
|
66
|
+
|| (effectiveTransactionId !== undefined && effectiveTransactionId !== purchase.transactionId)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
transactionId: purchase.transactionId,
|
|
71
|
+
productId: input.productId,
|
|
72
|
+
status: 'granted',
|
|
73
|
+
ledgerEntryId: purchase.authoritativeGrant.ledgerEntryId,
|
|
74
|
+
...(purchase.authoritativeGrant.alreadyProcessed === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { alreadyProcessed: purchase.authoritativeGrant.alreadyProcessed }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
44
79
|
export function resolveShareCompletion(result) {
|
|
45
80
|
if (result.status !== 'shared') {
|
|
46
81
|
return undefined;
|
package/package.json
CHANGED