@mpgd/adapter-capacitor 0.17.0 → 0.19.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 +55 -13
- package/dist/commerce-bridge.d.ts +32 -1
- package/dist/commerce-bridge.js +96 -4
- package/dist/index.d.ts +11 -2
- package/dist/index.js +139 -7
- package/dist/providers.d.ts +10 -2
- package/dist/purchase-presentation.d.ts +55 -0
- package/dist/purchase-presentation.js +170 -0
- package/dist/store-commerce-provider.d.ts +16 -2
- package/dist/store-commerce-provider.js +229 -63
- package/dist/store-sdk.d.ts +66 -2
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -148,14 +148,11 @@ seconds): the sales flag is unknown rather than closed. An answer that
|
|
|
148
148
|
arrives after the timeout is ignored and never opens the store. A thrown
|
|
149
149
|
`CommerceError` keeps its own code, except a code that claims a launched or
|
|
150
150
|
cancelled sheet (`OUTCOME_UNKNOWN`, `ALREADY_OWNED`, `USER_CANCELLED`), which
|
|
151
|
-
becomes `PURCHASE_GATE_FAILED` because nothing was launched.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
idempotency key. The
|
|
156
|
-
that could only ever say `not-started`, because the game runtime's purchase
|
|
157
|
-
coordinator would take it as a closure source and quarantine the game after
|
|
158
|
-
every launched purchase. The gate runs while the gateway already holds the
|
|
151
|
+
becomes `PURCHASE_GATE_FAILED` because nothing was launched. Over an SDK
|
|
152
|
+
with sheet facts (below), a refusal is reported `not-started` before the
|
|
153
|
+
answer, so the reconciler keeps a retryable refusal's key reusable and the
|
|
154
|
+
same key asks again; without sheet facts the reconciler journals the refused
|
|
155
|
+
attempt as terminal `failed`, and a later try uses a new idempotency key. The gate runs while the gateway already holds the
|
|
159
156
|
purchase's external activity (the game is paused, with no sheet shown), so it
|
|
160
157
|
should answer from a flag the game already holds rather than a slow network
|
|
161
158
|
call; the timeout bounds that pause. Recovery, restore, finishing, events, `fetchProducts`, and
|
|
@@ -187,6 +184,50 @@ cannot advertise one the SDK lacks: `restore: true` without
|
|
|
187
184
|
`sdk.restorePurchases`, like `finish: 'client' | 'host'` without
|
|
188
185
|
`sdk.finishTransaction`, is rejected when the provider is created.
|
|
189
186
|
|
|
187
|
+
### Store sheet facts
|
|
188
|
+
|
|
189
|
+
A `StoreSdk` that declares `purchasePresentation` reports, for a
|
|
190
|
+
`requestPurchase` call that carries a `presentationId`, when the store sheet
|
|
191
|
+
opens and when it is gone (`open`, then `closed`), or that none opened
|
|
192
|
+
(`not-started`). The Play Billing and StoreKit SDKs declare it by default.
|
|
193
|
+
Only then does the provider offer `CommerceProvider.presentation`, so a new
|
|
194
|
+
adapter over an SDK from before the facts keeps the behavior below, and an
|
|
195
|
+
SDK never receives a `presentationId` from an adapter that predates them.
|
|
196
|
+
|
|
197
|
+
The provider answers every `requestPurchase` with facts keyed by its
|
|
198
|
+
idempotency key, numbered with one increasing `sequence`:
|
|
199
|
+
|
|
200
|
+
- `not-started`, delivered before the answer, for a call that opened no
|
|
201
|
+
sheet: a purchase gate or account binding refusal, an unavailable store,
|
|
202
|
+
any throw, the SDK's own `not-started`, or an SDK that settled without a
|
|
203
|
+
fact.
|
|
204
|
+
- `unknown` when the SDK answers while its native flow still runs and has
|
|
205
|
+
observed nothing (a call timeout): the call keeps waiting for the SDK's
|
|
206
|
+
`open`, `closed`, or `not-started`, bounded by the safety timeout below,
|
|
207
|
+
instead of reading the answer as either.
|
|
208
|
+
- Otherwise `open`, then exactly one `closed`: the SDK's, or after
|
|
209
|
+
`purchasePresentationTimeoutMs` (15 minutes, `reason: 'timeout'`), or on
|
|
210
|
+
`dispose()` (`reason: 'disposed'`). A `not-started` after `open` becomes
|
|
211
|
+
`closed`. A result, a pending purchase, an Ask to Buy, or a purchase event
|
|
212
|
+
is never a `closed` fact.
|
|
213
|
+
|
|
214
|
+
`createCommerceProviderBridge` serves the facts through
|
|
215
|
+
`subscribePresentation` as `{ protocol: 'mpgd.commerce.presentation.v1',
|
|
216
|
+
sequence, event }` envelopes, apart from the commerce envelopes of
|
|
217
|
+
`subscribe`, so a commerce listener (and the host's buffering of purchase
|
|
218
|
+
events for its first listener) is unchanged. `createBridgeCommerceProvider`
|
|
219
|
+
offers `presentation` only when its input has `subscribePresentation`; it
|
|
220
|
+
validates and renumbers the facts and adds `not-started` for a request the
|
|
221
|
+
bridge refused before the host's provider ran. The gateway passes
|
|
222
|
+
`subscribePresentation` only from a provider bridge that has it. With facts,
|
|
223
|
+
the game runtime's purchase coordination turns on by itself
|
|
224
|
+
(`createGamePlatformRuntime`: `createCoordinatedPlatformGateway` with an ad
|
|
225
|
+
provider, `createCoordinatedCommerceProvider` without one), and the gateway
|
|
226
|
+
keeps a purchase's pause (`beginExternalActivity()`) until the sheet's
|
|
227
|
+
`closed` or `not-started`, whatever the answer, with
|
|
228
|
+
`presentedPurchaseActivityTimeoutMs` as a safety net: an Ask to Buy whose
|
|
229
|
+
sheet closed holds nothing.
|
|
230
|
+
|
|
190
231
|
`createCommerceServiceProvider({ id, commerce })` wraps the provider as a
|
|
191
232
|
`CapacitorServiceProvider` whose `commerce` field the registry recognizes. It
|
|
192
233
|
declares no `features` and no `methods`: the six `commerce.v2.*` bridge methods
|
|
@@ -210,11 +251,12 @@ read per snapshot (`getCapabilitySnapshot` returns `{ availability, commerce }`,
|
|
|
210
251
|
three-second timeout, shape validated): a stalled, failing, or invalid
|
|
211
252
|
provider yields a degraded block (`createDegradedCommerceCapabilities`:
|
|
212
253
|
`availability: 'temporarily-unavailable'`, nothing sold) and never rejects the
|
|
213
|
-
capability read.
|
|
214
|
-
after the request returns, since the store UI
|
|
215
|
-
it when the provider reports that
|
|
216
|
-
`
|
|
217
|
-
on `lifecycle.dispose`.
|
|
254
|
+
capability read. Without sheet facts, a `launched` purchase keeps
|
|
255
|
+
`beginExternalActivity()` open after the request returns, since the store UI
|
|
256
|
+
may still be up: the gateway releases it when the provider reports that
|
|
257
|
+
product (`purchaseUpdated` or `purchaseError`), after
|
|
258
|
+
`launchedPurchaseActivityTimeoutMs` (two minutes), or on `lifecycle.dispose`.
|
|
259
|
+
With facts, the sheet's own `closed` ends the pause (see above). The registry does not gate a commerce method by a
|
|
218
260
|
snapshot: the provider reads its own `availability` and fails closed before any
|
|
219
261
|
store UI opens. A `commerce.v2.*` request that reaches a transport without a
|
|
220
262
|
provider still answers `COMMERCE_V2_UNAVAILABLE`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BridgeCommerceEvent, type BridgeCommerceV2Method, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
1
|
+
import { type BridgeCommerceEvent, type BridgeCommercePresentationEvent, type BridgeCommerceV2Method, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
2
2
|
import { type CommerceBridgeMethods, type CommerceProvider, type StoreId } from '@mpgd/platform/commerce';
|
|
3
3
|
import type { CapacitorCommerceServiceProvider } from './providers.js';
|
|
4
4
|
/** Bridge error for a commerce v2 payload the typia assert rejected. */
|
|
@@ -9,6 +9,16 @@ export interface CommerceProviderBridge {
|
|
|
9
9
|
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
10
10
|
/** Delivers `{ protocol: 'mpgd.commerce.v2', sequence, event }`; `sequence` starts at 0 per bridge. */
|
|
11
11
|
subscribe(listener: (event: BridgeCommerceEvent) => void): () => void;
|
|
12
|
+
/**
|
|
13
|
+
* Present when the provider has `presentation`: delivers its purchase
|
|
14
|
+
* presentation facts as `{ protocol: 'mpgd.commerce.presentation.v1',
|
|
15
|
+
* sequence, event }`, `sequence` from 0 per bridge. Facts are never
|
|
16
|
+
* buffered, and a fact of a call that opened no sheet reaches a listener
|
|
17
|
+
* before that call's answer. Kept apart from `subscribe` so a commerce
|
|
18
|
+
* listener sees only commerce envelopes, and a bridge without it tells a
|
|
19
|
+
* consumer that its host has no facts.
|
|
20
|
+
*/
|
|
21
|
+
subscribePresentation?(listener: (event: BridgeCommercePresentationEvent) => void): () => void;
|
|
12
22
|
}
|
|
13
23
|
export interface CreateCommerceProviderBridgeInput {
|
|
14
24
|
readonly provider: CommerceProvider;
|
|
@@ -51,11 +61,32 @@ export interface CreateBridgeCommerceProviderInput {
|
|
|
51
61
|
* provider does not declare it and callers never send `retry`.
|
|
52
62
|
*/
|
|
53
63
|
readonly acceptsFinishRetry?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The bridge host accepts `FinishTransactionInput.revoked` (same contract,
|
|
66
|
+
* and its provider declares `acceptsRevokedFinish`). Absent, the provider
|
|
67
|
+
* does not declare it and callers never send `revoked`.
|
|
68
|
+
*/
|
|
69
|
+
readonly acceptsRevokedFinish?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The host's purchase presentation facts
|
|
72
|
+
* (`CommerceProviderBridge.subscribePresentation`), each fact of a call
|
|
73
|
+
* that opened no sheet delivered before that call's answer. Only with it
|
|
74
|
+
* does this provider offer `presentation`: a host built before the
|
|
75
|
+
* protocol, or one whose provider has no facts, leaves it out, and the
|
|
76
|
+
* consumer keeps its behavior without them.
|
|
77
|
+
*/
|
|
78
|
+
readonly subscribePresentation?: (listener: (event: BridgeCommercePresentationEvent) => void) => () => void;
|
|
54
79
|
}
|
|
55
80
|
/**
|
|
56
81
|
* `CommerceProvider` over a bridge that serves the `commerce.v2.*` methods and
|
|
57
82
|
* the event envelope. Results are validated again on this side, so a transport
|
|
58
83
|
* that forgets to is still fail-closed; bridge errors become `CommerceError`s
|
|
59
84
|
* with the bridge code kept as `storeCode` when it is not already a v2 code.
|
|
85
|
+
*
|
|
86
|
+
* With `subscribePresentation`, it offers `presentation`: the host's facts,
|
|
87
|
+
* validated and numbered again on this side, plus `not-started`, delivered
|
|
88
|
+
* before the answer, for a `requestPurchase` that settles without any fact
|
|
89
|
+
* (the bridge refused it before the host's provider ran). Every call it
|
|
90
|
+
* forwards then ends with a fact.
|
|
60
91
|
*/
|
|
61
92
|
export declare function createBridgeCommerceProvider(input: CreateBridgeCommerceProviderInput): CommerceProvider;
|
package/dist/commerce-bridge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { bridgeCommerceEventProtocol, createBridgeError, isBridgeCommerceV2Method, } from '@mpgd/bridge';
|
|
2
|
-
import { PlatformOperationError } from '@mpgd/platform';
|
|
3
|
-
import { assertCommerceBridgeFetchProductsPayload, assertCommerceBridgeFetchProductsResult, assertCommerceBridgeFinishTransactionPayload, assertCommerceBridgeFinishTransactionResult, assertCommerceBridgeGetAvailablePurchasesPayload, assertCommerceBridgeGetAvailablePurchasesResult, assertCommerceBridgeGetCapabilitiesPayload, assertCommerceBridgeGetCapabilitiesResult, assertCommerceBridgeRequestPurchasePayload, assertCommerceBridgeRequestPurchaseResult, assertCommerceBridgeRestorePurchasesPayload, assertCommerceBridgeRestorePurchasesResult, assertCommerceEventEnvelope, CommerceError, commerceErrorInfoOf, commerceProtocol, commerceProtocolVersion, } from '@mpgd/platform/commerce';
|
|
1
|
+
import { bridgeCommerceEventProtocol, bridgeCommercePresentationEventProtocol, createBridgeError, isBridgeCommerceV2Method, } from '@mpgd/bridge';
|
|
2
|
+
import { createPurchasePresentationRelay, PlatformOperationError, } from '@mpgd/platform';
|
|
3
|
+
import { assertCommerceBridgeFetchProductsPayload, assertCommerceBridgeFetchProductsResult, assertCommerceBridgeFinishTransactionPayload, assertCommerceBridgeFinishTransactionResult, assertCommerceBridgeGetAvailablePurchasesPayload, assertCommerceBridgeGetAvailablePurchasesResult, assertCommerceBridgeGetCapabilitiesPayload, assertCommerceBridgeGetCapabilitiesResult, assertCommerceBridgeRequestPurchasePayload, assertCommerceBridgeRequestPurchaseResult, assertCommerceBridgeRestorePurchasesPayload, assertCommerceBridgeRestorePurchasesResult, assertCommerceEventEnvelope, assertCommercePresentationEventEnvelope, CommerceError, commerceErrorInfoOf, commerceProtocol, commerceProtocolVersion, } from '@mpgd/platform/commerce';
|
|
4
4
|
import { namesOtherProtocol } from './bridge-events.js';
|
|
5
5
|
import { normalizeStoreSdkError } from './store-sdk.js';
|
|
6
6
|
/** Bridge error for a commerce v2 payload the typia assert rejected. */
|
|
@@ -19,8 +19,11 @@ export const commerceV2MethodUnsupportedBridgeErrorCode = 'COMMERCE_V2_METHOD_UN
|
|
|
19
19
|
export function createCommerceProviderBridge(input) {
|
|
20
20
|
const { provider } = input;
|
|
21
21
|
const listeners = new Set();
|
|
22
|
+
const presentationListeners = new Set();
|
|
22
23
|
let sequence = 0;
|
|
24
|
+
let presentationSequence = 0;
|
|
23
25
|
let detach;
|
|
26
|
+
let detachPresentation;
|
|
24
27
|
const deliver = (event) => {
|
|
25
28
|
const envelope = { protocol: commerceProtocol, sequence, event };
|
|
26
29
|
sequence += 1;
|
|
@@ -28,6 +31,35 @@ export function createCommerceProviderBridge(input) {
|
|
|
28
31
|
listener(envelope);
|
|
29
32
|
}
|
|
30
33
|
};
|
|
34
|
+
const deliverPresentation = (event) => {
|
|
35
|
+
let envelope;
|
|
36
|
+
try {
|
|
37
|
+
envelope = assertCommercePresentationEventEnvelope({
|
|
38
|
+
protocol: bridgeCommercePresentationEventProtocol,
|
|
39
|
+
sequence: presentationSequence,
|
|
40
|
+
event,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// A fact the contract rejects is not delivered; the provider's own
|
|
45
|
+
// safety timeout still ends a sheet it never reports closed.
|
|
46
|
+
console.warn('A commerce provider reported an invalid presentation fact.', error);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
presentationSequence += 1;
|
|
50
|
+
for (const entry of [...presentationListeners]) {
|
|
51
|
+
if (!presentationListeners.has(entry)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
entry.listener(envelope);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
// A failing listener must not keep a `closed` fact from the others.
|
|
59
|
+
console.warn('A purchase presentation bridge listener failed.', error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
31
63
|
const serve = async (request) => {
|
|
32
64
|
const { payload } = request;
|
|
33
65
|
switch (request.method) {
|
|
@@ -101,6 +133,22 @@ export function createCommerceProviderBridge(input) {
|
|
|
101
133
|
detach = undefined;
|
|
102
134
|
};
|
|
103
135
|
},
|
|
136
|
+
...(provider.presentation === undefined ? {} : {
|
|
137
|
+
subscribePresentation(listener) {
|
|
138
|
+
const entry = { listener };
|
|
139
|
+
presentationListeners.add(entry);
|
|
140
|
+
if (presentationListeners.size === 1) {
|
|
141
|
+
detachPresentation = provider.presentation?.subscribe(deliverPresentation);
|
|
142
|
+
}
|
|
143
|
+
return () => {
|
|
144
|
+
if (!presentationListeners.delete(entry) || presentationListeners.size > 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
detachPresentation?.();
|
|
148
|
+
detachPresentation = undefined;
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
104
152
|
};
|
|
105
153
|
}
|
|
106
154
|
/**
|
|
@@ -164,6 +212,12 @@ function isTypiaGuardError(error) {
|
|
|
164
212
|
* the event envelope. Results are validated again on this side, so a transport
|
|
165
213
|
* that forgets to is still fail-closed; bridge errors become `CommerceError`s
|
|
166
214
|
* with the bridge code kept as `storeCode` when it is not already a v2 code.
|
|
215
|
+
*
|
|
216
|
+
* With `subscribePresentation`, it offers `presentation`: the host's facts,
|
|
217
|
+
* validated and numbered again on this side, plus `not-started`, delivered
|
|
218
|
+
* before the answer, for a `requestPurchase` that settles without any fact
|
|
219
|
+
* (the bridge refused it before the host's provider ran). Every call it
|
|
220
|
+
* forwards then ends with a fact.
|
|
167
221
|
*/
|
|
168
222
|
export function createBridgeCommerceProvider(input) {
|
|
169
223
|
const send = async (method, payload) => {
|
|
@@ -184,9 +238,10 @@ export function createBridgeCommerceProvider(input) {
|
|
|
184
238
|
});
|
|
185
239
|
}
|
|
186
240
|
};
|
|
187
|
-
|
|
241
|
+
const provider = {
|
|
188
242
|
store: input.store,
|
|
189
243
|
...(input.acceptsFinishRetry === true ? { acceptsFinishRetry: true } : {}),
|
|
244
|
+
...(input.acceptsRevokedFinish === true ? { acceptsRevokedFinish: true } : {}),
|
|
190
245
|
async getCapabilities() {
|
|
191
246
|
return (await send('commerce.v2.getCapabilities', { protocol: commerceProtocol }))
|
|
192
247
|
.capabilities;
|
|
@@ -232,6 +287,43 @@ export function createBridgeCommerceProvider(input) {
|
|
|
232
287
|
});
|
|
233
288
|
},
|
|
234
289
|
};
|
|
290
|
+
const subscribePresentation = input.subscribePresentation;
|
|
291
|
+
return subscribePresentation === undefined
|
|
292
|
+
? provider
|
|
293
|
+
: withBridgePresentation(provider, subscribePresentation);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* `presentation` over the host's facts: validated, then relayed by the
|
|
297
|
+
* platform's `createPurchasePresentationRelay` (a superseded fact dropped,
|
|
298
|
+
* the rest numbered again, `not-started` added for a call that settled
|
|
299
|
+
* without a fact). One attachment to the host serves every listener.
|
|
300
|
+
*/
|
|
301
|
+
function withBridgePresentation(provider, subscribePresentation) {
|
|
302
|
+
const relay = createPurchasePresentationRelay({
|
|
303
|
+
subscribe: (listener) => subscribePresentation((envelope) => {
|
|
304
|
+
if (namesOtherProtocol(envelope, bridgeCommercePresentationEventProtocol)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
let event;
|
|
308
|
+
try {
|
|
309
|
+
event = assertCommercePresentationEventEnvelope(envelope).event;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
console.warn('The bridge delivered an invalid purchase presentation fact.', error);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
listener(event);
|
|
316
|
+
}),
|
|
317
|
+
}, {
|
|
318
|
+
onListenerError: (error) => {
|
|
319
|
+
console.warn('A purchase presentation listener failed.', error);
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
...provider,
|
|
324
|
+
presentation: relay.presentation,
|
|
325
|
+
requestPurchase: (request) => relay.track(request.idempotencyKey, () => provider.requestPurchase(request)),
|
|
326
|
+
};
|
|
235
327
|
}
|
|
236
328
|
function toCommerceError(error) {
|
|
237
329
|
if (error instanceof CommerceError) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,19 +3,28 @@ import { type CapacitorAppEventsApi, type CapacitorIncomingUrlKind, type Capacit
|
|
|
3
3
|
import { type CapacitorCrashReportingOptions } from './crash-reporting.js';
|
|
4
4
|
import { type CapacitorServiceProvider, type NativeBridge } from './providers.js';
|
|
5
5
|
export { capacitorCrashlyticsPluginName, capacitorCrashReportingProvider, createCapacitorCrashReporter, createLocalCrashCollectionPreferenceStore, type CrashCollectionPreferenceStore, type CapacitorCrashlyticsPlugin, type CapacitorCrashReporterOptions, type CapacitorCrashReportingOptions, } from './crash-reporting.js';
|
|
6
|
+
export { createNativePurchasePresentation, purchasePresentationTimeoutMs, type NativePurchasePresentation, type NativePurchasePresentationPlugin, } from './purchase-presentation.js';
|
|
6
7
|
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, createDegradedNotificationCapabilities, type CapacitorCommerceServiceProvider, type CapacitorNotificationServiceProvider, type CapacitorNotificationsV2Service, type CapacitorProviderCapabilitySnapshot, type CapacitorProviderRegistry, type CapacitorServiceProvider, type NativeBridge, } from './providers.js';
|
|
7
8
|
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, type CommerceProviderBridge, type CreateBridgeCommerceProviderInput, type CreateCommerceProviderBridgeInput, type CreateCommerceServiceProviderInput, } from './commerce-bridge.js';
|
|
8
9
|
export { accountBindingLookupFailedStoreCode, createStoreCommerceProvider, purchaseGateFailedStoreCode, purchaseGateTimeoutMs, purchasesPausedStoreCode, recoverableErrorCodes, type CreateStoreCommerceProviderInput, type StoreCommerceCapabilityOverrides, type StoreCommerceProductMapping, type StoreCommerceProvider, type StorePurchaseGate, type StorePurchaseGateDecision, type StorePurchaseGateRequest, } from './store-commerce-provider.js';
|
|
9
|
-
export { defaultStoreSdkErrorCodeMap, normalizeStoreSdkError, type StoreSdk, type StoreSdkCapabilities, type StoreSdkError, type StoreSdkErrorCodeMap, type StoreSdkEvent, type StoreSdkEventName, type StoreSdkEventOf, type StoreSdkFetchProductsInput, type StoreSdkFinishTransactionInput, type StoreSdkFinishTransactionResult, type StoreSdkGetAvailablePurchasesInput, type StoreSdkProduct, type StoreSdkPurchase, type StoreSdkRequestPurchaseInput, type StoreSdkRequestPurchaseResult, } from './store-sdk.js';
|
|
10
|
+
export { defaultStoreSdkErrorCodeMap, normalizeStoreSdkError, type StoreSdk, type StoreSdkCapabilities, type StoreSdkError, type StoreSdkErrorCodeMap, type StoreSdkEvent, type StoreSdkEventName, type StoreSdkEventOf, type StoreSdkFetchProductsInput, type StoreSdkFinishTransactionInput, type StoreSdkGrantedFinishTransactionInput, type StoreSdkRevokedFinishTransactionInput, type StoreSdkFinishTransactionResult, type StoreSdkGetAvailablePurchasesInput, type StoreSdkProduct, type StoreSdkPurchase, type StoreSdkRequestPurchaseInput, type StoreSdkRequestPurchaseResult, } from './store-sdk.js';
|
|
10
11
|
export { createCapacitorAppEvents, type CapacitorAppEventsApi, type CapacitorIncomingUrlKind, type CapacitorVisibilitySource, } from './app-events.js';
|
|
11
12
|
export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, type CapacitorJsonRequest, type CapacitorJsonResponse, type CapacitorNativeHttpErrorCode, type CapacitorNativeJsonTransport, type CreateCapacitorNativeJsonTransportInput, } from './native-http.js';
|
|
12
13
|
export { createCapacitorViewport, type CapacitorViewportBaseState, type CapacitorOccupiedSurfaceInput, type CapacitorViewportController, type CapacitorViewportHost, } from './viewport.js';
|
|
13
14
|
/**
|
|
14
15
|
* Upper bound on how long a `launched` commerce v2 purchase keeps the game
|
|
15
16
|
* paused when no purchase event for the product arrives (for example a store
|
|
16
|
-
* sheet dismissed without a callback).
|
|
17
|
+
* sheet dismissed without a callback). Only for a provider without sheet
|
|
18
|
+
* facts; see `presentedPurchaseActivityTimeoutMs`.
|
|
17
19
|
*/
|
|
18
20
|
export declare const launchedPurchaseActivityTimeoutMs = 120000;
|
|
21
|
+
/**
|
|
22
|
+
* Safety bound on how long a purchase whose provider reports sheet facts
|
|
23
|
+
* keeps the game paused after the purchase call answered with the sheet
|
|
24
|
+
* still open. The sheet's `closed` fact normally ends the pause; the
|
|
25
|
+
* provider's own `purchasePresentationTimeoutMs` reports one first.
|
|
26
|
+
*/
|
|
27
|
+
export declare const presentedPurchaseActivityTimeoutMs: number;
|
|
19
28
|
export declare function createCapacitorPlatformGateway(input: {
|
|
20
29
|
readonly target: Extract<PlatformTarget, 'android' | 'ios'>;
|
|
21
30
|
readonly appVersion: string;
|
package/dist/index.js
CHANGED
|
@@ -4,12 +4,14 @@ import { PlatformOperationError, } from '@mpgd/platform';
|
|
|
4
4
|
import { withShareIntentCheck } from '@mpgd/platform/routes';
|
|
5
5
|
import { createCapacitorAppEvents, } from './app-events.js';
|
|
6
6
|
import { namesOtherProtocol } from './bridge-events.js';
|
|
7
|
+
import { purchasePresentationTimeoutMs } from './purchase-presentation.js';
|
|
7
8
|
import { createBridgeCommerceProvider } from './commerce-bridge.js';
|
|
8
9
|
import { createCapacitorGatewayCrashReporting, } from './crash-reporting.js';
|
|
9
10
|
import { assertProviderResponseData, createCapacitorProviderRegistry, } from './providers.js';
|
|
10
11
|
import { assertNotificationsV2Served, externalActivityMethods } from './request-policy.js';
|
|
11
12
|
import { createCapacitorViewport } from './viewport.js';
|
|
12
13
|
export { capacitorCrashlyticsPluginName, capacitorCrashReportingProvider, createCapacitorCrashReporter, createLocalCrashCollectionPreferenceStore, } from './crash-reporting.js';
|
|
14
|
+
export { createNativePurchasePresentation, purchasePresentationTimeoutMs, } from './purchase-presentation.js';
|
|
13
15
|
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, createDegradedNotificationCapabilities, } from './providers.js';
|
|
14
16
|
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, } from './commerce-bridge.js';
|
|
15
17
|
export { accountBindingLookupFailedStoreCode, createStoreCommerceProvider, purchaseGateFailedStoreCode, purchaseGateTimeoutMs, purchasesPausedStoreCode, recoverableErrorCodes, } from './store-commerce-provider.js';
|
|
@@ -23,9 +25,17 @@ function isRecord(value) {
|
|
|
23
25
|
/**
|
|
24
26
|
* Upper bound on how long a `launched` commerce v2 purchase keeps the game
|
|
25
27
|
* paused when no purchase event for the product arrives (for example a store
|
|
26
|
-
* sheet dismissed without a callback).
|
|
28
|
+
* sheet dismissed without a callback). Only for a provider without sheet
|
|
29
|
+
* facts; see `presentedPurchaseActivityTimeoutMs`.
|
|
27
30
|
*/
|
|
28
31
|
export const launchedPurchaseActivityTimeoutMs = 120_000;
|
|
32
|
+
/**
|
|
33
|
+
* Safety bound on how long a purchase whose provider reports sheet facts
|
|
34
|
+
* keeps the game paused after the purchase call answered with the sheet
|
|
35
|
+
* still open. The sheet's `closed` fact normally ends the pause; the
|
|
36
|
+
* provider's own `purchasePresentationTimeoutMs` reports one first.
|
|
37
|
+
*/
|
|
38
|
+
export const presentedPurchaseActivityTimeoutMs = purchasePresentationTimeoutMs;
|
|
29
39
|
const safeFallbackMethods = new Set([
|
|
30
40
|
'identity.getPlayer',
|
|
31
41
|
'identity.getSession',
|
|
@@ -59,12 +69,14 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
59
69
|
// other provider method, so the bridge envelope, response validation, and
|
|
60
70
|
// external activity bookkeeping are exercised in process exactly as over a
|
|
61
71
|
// transport.
|
|
72
|
+
const subscribePresentation = commerceProvider?.bridge.subscribePresentation?.bind(commerceProvider.bridge);
|
|
62
73
|
const commerce = commerceProvider === undefined
|
|
63
74
|
? undefined
|
|
64
75
|
: createBridgeCommerceProvider({
|
|
65
76
|
store: commerceProvider.commerce.store,
|
|
66
77
|
// The host below is this package's, on the same contract.
|
|
67
78
|
acceptsFinishRetry: commerceProvider.commerce.acceptsFinishRetry === true,
|
|
79
|
+
acceptsRevokedFinish: commerceProvider.commerce.acceptsRevokedFinish === true,
|
|
68
80
|
request: (method, payload) => request(method, payload),
|
|
69
81
|
// The provider's channel (`NativeBridge`) may carry other protocols.
|
|
70
82
|
// Only an envelope that names another protocol is dropped; anything
|
|
@@ -75,6 +87,10 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
75
87
|
listener(envelope);
|
|
76
88
|
}
|
|
77
89
|
}),
|
|
90
|
+
// Facts only from a bridge that serves them (`createCommerceServiceProvider`
|
|
91
|
+
// over a provider with `presentation`); otherwise `commerce` has no
|
|
92
|
+
// `presentation` and purchases keep the result-based pause below.
|
|
93
|
+
...(subscribePresentation === undefined ? {} : { subscribePresentation }),
|
|
78
94
|
});
|
|
79
95
|
const notificationsProvider = providers.notifications;
|
|
80
96
|
// Served through the same request path as every other provider method, as
|
|
@@ -137,6 +153,22 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
137
153
|
? lifecycle.beginExternalActivity?.()
|
|
138
154
|
: undefined;
|
|
139
155
|
let holdActivity = false;
|
|
156
|
+
// Watched before the request is sent: a provider reports a sheet that
|
|
157
|
+
// never opened before it answers.
|
|
158
|
+
const presentation = releaseExternalActivity !== undefined
|
|
159
|
+
&& method === 'commerce.v2.requestPurchase'
|
|
160
|
+
? watchPurchasePresentation(payload)
|
|
161
|
+
: undefined;
|
|
162
|
+
// Released once: by the sheet's own end (which may come while the call
|
|
163
|
+
// still runs, for example a safety-timeout `closed`), or after the call.
|
|
164
|
+
let released = false;
|
|
165
|
+
const release = () => {
|
|
166
|
+
if (!released) {
|
|
167
|
+
released = true;
|
|
168
|
+
releaseExternalActivity?.();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
presentation?.onEnd(release);
|
|
140
172
|
try {
|
|
141
173
|
const provider = leaderboardRoute === 'remote'
|
|
142
174
|
&& (method === 'leaderboard.submitScore' || method === 'leaderboard.open')
|
|
@@ -148,11 +180,15 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
148
180
|
const data = await sendRequest(method, payload, provider);
|
|
149
181
|
if (releaseExternalActivity !== undefined
|
|
150
182
|
&& method === 'commerce.v2.requestPurchase'
|
|
151
|
-
&& isRecord(data) && data.outcome === 'launched'
|
|
152
|
-
|
|
153
|
-
//
|
|
183
|
+
&& isRecord(data) && data.outcome === 'launched'
|
|
184
|
+
&& presentation?.reported() !== true) {
|
|
185
|
+
// No sheet facts for this purchase: the store UI may still be
|
|
186
|
+
// open, so stay paused until this product's purchase event
|
|
187
|
+
// arrives or the safety timeout ends. With facts, the sheet's
|
|
188
|
+
// own `closed` ends the pause (below), and a pending purchase
|
|
189
|
+
// whose sheet closed holds nothing.
|
|
154
190
|
holdActivity = true;
|
|
155
|
-
holdLaunchedPurchaseActivity(
|
|
191
|
+
holdLaunchedPurchaseActivity(release, isRecord(payload) ? payload.logicalId : undefined);
|
|
156
192
|
}
|
|
157
193
|
return data;
|
|
158
194
|
}
|
|
@@ -168,10 +204,106 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
168
204
|
return await sendRequest(method, payload, undefined);
|
|
169
205
|
}
|
|
170
206
|
finally {
|
|
171
|
-
if (
|
|
172
|
-
|
|
207
|
+
if (holdActivity) {
|
|
208
|
+
presentation?.dispose();
|
|
209
|
+
}
|
|
210
|
+
else if (!released && presentation?.open() === true) {
|
|
211
|
+
// The sheet is still up, whatever the answer (a result, a pending
|
|
212
|
+
// purchase, or a throw): stay paused until its `closed` fact.
|
|
213
|
+
holdPresentedPurchaseActivity(release, presentation);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
presentation?.dispose();
|
|
217
|
+
release();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Follows one purchase's presentation facts (by its idempotency key) from
|
|
223
|
+
* `commerce.presentation`: the bridge's facts already validated, with a
|
|
224
|
+
* superseded or replayed one dropped, so a stale fact cannot end the hold
|
|
225
|
+
* while the sheet is up. Undefined when the bridge serves no facts or the
|
|
226
|
+
* payload has no key.
|
|
227
|
+
*/
|
|
228
|
+
function watchPurchasePresentation(payload) {
|
|
229
|
+
const idempotencyKey = isRecord(payload) ? payload.idempotencyKey : undefined;
|
|
230
|
+
const facts = commerce?.presentation;
|
|
231
|
+
if (facts === undefined || typeof idempotencyKey !== 'string') {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
// `held`: the sheet is (`open`) or may be (`unknown`) up; `opened`: seen open.
|
|
235
|
+
let held = false;
|
|
236
|
+
let opened = false;
|
|
237
|
+
let ended = false;
|
|
238
|
+
let onEnd;
|
|
239
|
+
let unsubscribe;
|
|
240
|
+
let disposed = false;
|
|
241
|
+
const dispose = () => {
|
|
242
|
+
disposed = true;
|
|
243
|
+
onEnd = undefined;
|
|
244
|
+
unsubscribe?.();
|
|
245
|
+
unsubscribe = undefined;
|
|
246
|
+
};
|
|
247
|
+
const subscription = facts.subscribe((event) => {
|
|
248
|
+
if (event.idempotencyKey !== idempotencyKey || ended) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const { state } = event;
|
|
252
|
+
if (state === 'open' || state === 'unknown') {
|
|
253
|
+
// `unknown`: the sheet may still be up; hold as for an open one.
|
|
254
|
+
held = true;
|
|
255
|
+
opened ||= state === 'open';
|
|
173
256
|
}
|
|
257
|
+
else if (state === 'closed' || (state === 'not-started' && !opened)) {
|
|
258
|
+
// A sheet seen open ends only with `closed` (its provider turns a
|
|
259
|
+
// later `not-started` into `closed`); a `not-started` then belongs
|
|
260
|
+
// to another call of the same key.
|
|
261
|
+
ended = true;
|
|
262
|
+
const end = onEnd;
|
|
263
|
+
dispose();
|
|
264
|
+
end?.();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
if (disposed) {
|
|
268
|
+
subscription();
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
unsubscribe = subscription;
|
|
174
272
|
}
|
|
273
|
+
return {
|
|
274
|
+
reported: () => held || ended,
|
|
275
|
+
open: () => held && !ended,
|
|
276
|
+
onEnd(callback) {
|
|
277
|
+
if (ended) {
|
|
278
|
+
callback();
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
onEnd = callback;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
dispose,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Keeps a purchase's external activity open while its store sheet is up:
|
|
289
|
+
* until the sheet's `closed` fact, the safety timeout, or dispose. A
|
|
290
|
+
* result, a pending purchase, or a purchase event does not end it.
|
|
291
|
+
*/
|
|
292
|
+
function holdPresentedPurchaseActivity(release, presentation) {
|
|
293
|
+
let done = false;
|
|
294
|
+
const finish = () => {
|
|
295
|
+
if (done) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
done = true;
|
|
299
|
+
launchedPurchaseHolds.delete(finish);
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
presentation.dispose();
|
|
302
|
+
release();
|
|
303
|
+
};
|
|
304
|
+
launchedPurchaseHolds.add(finish);
|
|
305
|
+
const timer = setTimeout(finish, presentedPurchaseActivityTimeoutMs);
|
|
306
|
+
presentation.onEnd(finish);
|
|
175
307
|
}
|
|
176
308
|
/**
|
|
177
309
|
* Keeps the external activity of a `launched` purchase open until the
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BridgeEvent, type BridgeMethod, type BridgeNotificationsEvent, type BridgeNotificationsV2Method, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
1
|
+
import { type BridgeCommercePresentationEvent, type BridgeEvent, type BridgeMethod, type BridgeNotificationsEvent, type BridgeNotificationsV2Method, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
2
2
|
import type { AdProvider } from '@mpgd/platform/ads';
|
|
3
3
|
import { type CommerceCapabilities, type NotificationSubscriptionAdapter, type PlatformProviderAvailability, type PlatformProviderFeature } from '@mpgd/platform';
|
|
4
4
|
import { type CommerceProvider } from '@mpgd/platform/commerce';
|
|
@@ -86,7 +86,15 @@ export interface CapacitorNotificationServiceProvider extends CapacitorServicePr
|
|
|
86
86
|
/** The provider the registry serves commerce v2 from. */
|
|
87
87
|
export interface CapacitorCommerceServiceProvider extends CapacitorServiceProvider {
|
|
88
88
|
readonly commerce: CommerceProvider;
|
|
89
|
-
readonly bridge: NativeBridge & Required<Pick<NativeBridge, 'subscribe'
|
|
89
|
+
readonly bridge: NativeBridge & Required<Pick<NativeBridge, 'subscribe'>> & {
|
|
90
|
+
/**
|
|
91
|
+
* The provider's purchase presentation facts
|
|
92
|
+
* (`CommerceProviderBridge.subscribePresentation`). Only with it does
|
|
93
|
+
* `gateway.commerce` offer `presentation`, and the gateway keep a
|
|
94
|
+
* purchase's pause until the sheet's `closed` fact.
|
|
95
|
+
*/
|
|
96
|
+
subscribePresentation?(listener: (event: BridgeCommercePresentationEvent) => void): () => void;
|
|
97
|
+
};
|
|
90
98
|
}
|
|
91
99
|
export interface CapacitorProviderCapabilitySnapshot {
|
|
92
100
|
readonly availability: Readonly<Record<PlatformProviderFeature, PlatformProviderAvailability>>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
|
+
import type { StoreSdkPurchasePresentation } from './store-sdk.js';
|
|
3
|
+
/**
|
|
4
|
+
* Upper bound on how long one store sheet counts as open (or possibly open)
|
|
5
|
+
* without a native `closed` fact. Past it the provider reports `closed` with
|
|
6
|
+
* reason `timeout`, so a lost native observation can pause the game, but
|
|
7
|
+
* never forever. It matches the purchase flow timeout of the OpenIAP Google
|
|
8
|
+
* core (15 minutes).
|
|
9
|
+
*/
|
|
10
|
+
export declare const purchasePresentationTimeoutMs: number;
|
|
11
|
+
/** A store plugin's native surface for sheet facts (Play Billing, StoreKit). */
|
|
12
|
+
export interface NativePurchasePresentationPlugin {
|
|
13
|
+
addListener(eventName: 'purchasePresentation', listener: (event: unknown) => void): Promise<PluginListenerHandle>;
|
|
14
|
+
readonly getPresentationSupport?: () => Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
/** What a store plugin's `StoreSdk` uses to report its native sheet facts. */
|
|
17
|
+
export interface NativePurchasePresentation {
|
|
18
|
+
/** The SDK's `purchasePresentation` declaration. */
|
|
19
|
+
readonly purchasePresentation: StoreSdkPurchasePresentation;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the native code in the app reports facts (`getPresentationSupport`).
|
|
22
|
+
* A build from before them answers `UNIMPLEMENTED`, which is remembered;
|
|
23
|
+
* any other failure is asked again next time.
|
|
24
|
+
*/
|
|
25
|
+
supported(): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Someone follows the facts: an adapter that reads them subscribes when it
|
|
28
|
+
* builds its provider (one from before them never does).
|
|
29
|
+
*/
|
|
30
|
+
followed(): boolean;
|
|
31
|
+
/** Follows facts for a `purchase` call about to be sent with this id. */
|
|
32
|
+
begin(presentationId: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Applies the sheet state a `purchase` answer, or a rejection's `data`,
|
|
35
|
+
* carried (`presentation`): `open`, `closed`, `not-started`, or
|
|
36
|
+
* `requested` (native code still runs the flow and has observed nothing
|
|
37
|
+
* yet; its facts follow as events).
|
|
38
|
+
*/
|
|
39
|
+
answered(presentationId: string, answerOrError: unknown): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Sheet facts by presentation id from the plugin's `purchasePresentation`
|
|
43
|
+
* events and the `presentation` field of its `purchase` answers, whichever
|
|
44
|
+
* arrives first; each call gets at most one `open` and one terminal fact.
|
|
45
|
+
* A `requested` answer is reported as `unknown`: the sheet may still come,
|
|
46
|
+
* so the provider keeps the call waiting for the native `open`, `closed`, or
|
|
47
|
+
* `not-started` instead of reading the answer as a sheet that never opened
|
|
48
|
+
* or inventing one that did. The native listener is attached with the first
|
|
49
|
+
* subscriber and removed with the last.
|
|
50
|
+
*
|
|
51
|
+
* Exported for the store plugins, which peer on this package. A plugin reads
|
|
52
|
+
* it through the module namespace, so with an adapter from before it the
|
|
53
|
+
* plugin declares no facts (and that adapter never asks for them).
|
|
54
|
+
*/
|
|
55
|
+
export declare function createNativePurchasePresentation(native: NativePurchasePresentationPlugin): NativePurchasePresentation;
|