@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/README.md +120 -9
- package/dist/app-store-server.d.ts +12 -12
- package/dist/app-store-server.js +10 -119
- package/dist/commerce-bridge.d.ts +59 -0
- package/dist/commerce-bridge.js +265 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +96 -6
- package/dist/providers.d.ts +52 -2
- package/dist/providers.js +158 -33
- package/dist/store-commerce-provider.d.ts +55 -0
- package/dist/store-commerce-provider.js +437 -0
- package/dist/store-sdk.d.ts +131 -0
- package/dist/store-sdk.js +83 -0
- package/package.json +7 -12
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', {}),
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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) => !
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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>;
|