@mpgd/adapter-capacitor 0.16.3 → 0.18.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 +88 -5
- 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 +141 -8
- 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 +67 -4
- package/dist/store-commerce-provider.js +301 -37
- package/dist/store-sdk.d.ts +41 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -133,6 +133,44 @@ normalizes every SDK failure through `mapError` into a `CommerceError` whose
|
|
|
133
133
|
`storeCode` keeps the store's own code. It never grants and never sets
|
|
134
134
|
`settlement`; the backend ledger decides.
|
|
135
135
|
|
|
136
|
+
`purchaseGate` is an optional sales switch for new purchases, separate from
|
|
137
|
+
the account binding (and from StoreKit's `appAccountToken`). It is asked once
|
|
138
|
+
per `requestPurchase`, after the product, availability, and kind checks and
|
|
139
|
+
before `accountBinding`, with `{ store, logicalId, storeSku, kind, source }`,
|
|
140
|
+
and answers `{ allowed: true }` or `{ allowed: false, storeCode?, message?,
|
|
141
|
+
retryable? }`. A refusal never opens the store UI and is answered as data:
|
|
142
|
+
`{ outcome: 'failed', error: { code: 'ITEM_UNAVAILABLE', storeCode:
|
|
143
|
+
'PURCHASES_PAUSED' } }` (or the gate's own `storeCode`), `retryable` false
|
|
144
|
+
unless the gate says otherwise. A gate that could not answer refuses too,
|
|
145
|
+
with `PURCHASE_GATE_FAILED` and always retryable, whether it threw, returned
|
|
146
|
+
something malformed, or did not answer within `purchaseGateTimeoutMs` (three
|
|
147
|
+
seconds): the sales flag is unknown rather than closed. An answer that
|
|
148
|
+
arrives after the timeout is ignored and never opens the store. A thrown
|
|
149
|
+
`CommerceError` keeps its own code, except a code that claims a launched or
|
|
150
|
+
cancelled sheet (`OUTCOME_UNKNOWN`, `ALREADY_OWNED`, `USER_CANCELLED`), which
|
|
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
|
|
156
|
+
purchase's external activity (the game is paused, with no sheet shown), so it
|
|
157
|
+
should answer from a flag the game already holds rather than a slow network
|
|
158
|
+
call; the timeout bounds that pause. Recovery, restore, finishing, events, `fetchProducts`, and
|
|
159
|
+
`getAvailablePurchases` never
|
|
160
|
+
ask the gate, so purchases already paid for keep reconciling while new sales
|
|
161
|
+
are refused. A failed or missing account binding is a `failed` result too,
|
|
162
|
+
not a throw: a thrown `CommerceError` keeps its code (with the same exception
|
|
163
|
+
for launched or cancelled codes), and anything else is
|
|
164
|
+
`ACCOUNT_BINDING_REQUIRED` with `ACCOUNT_BINDING_LOOKUP_FAILED`, retryable.
|
|
165
|
+
Over the bridge a thrown error keeps only `code`, `message`, and
|
|
166
|
+
`retryable`, while a result keeps `storeCode`, so the game can tell
|
|
167
|
+
`PURCHASES_PAUSED` from `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED`. The gate is not
|
|
168
|
+
reflected in `getCapabilities()`: `availability` describes the store and
|
|
169
|
+
gates the reconciler (a paused gate there would read as `STORE_UNAVAILABLE`
|
|
170
|
+
and lose the reason), and a capability read stays free of server round trips.
|
|
171
|
+
A shop hides its buy buttons from the same server flag it hands the gate,
|
|
172
|
+
for example the sales flag of its account binding response.
|
|
173
|
+
|
|
136
174
|
Two failure classes are not terminal. When `requestPurchase` throws or
|
|
137
175
|
returns a code that normalizes to `OUTCOME_UNKNOWN` (for example
|
|
138
176
|
`PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`,
|
|
@@ -146,6 +184,50 @@ cannot advertise one the SDK lacks: `restore: true` without
|
|
|
146
184
|
`sdk.restorePurchases`, like `finish: 'client' | 'host'` without
|
|
147
185
|
`sdk.finishTransaction`, is rejected when the provider is created.
|
|
148
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
|
+
|
|
149
231
|
`createCommerceServiceProvider({ id, commerce })` wraps the provider as a
|
|
150
232
|
`CapacitorServiceProvider` whose `commerce` field the registry recognizes. It
|
|
151
233
|
declares no `features` and no `methods`: the six `commerce.v2.*` bridge methods
|
|
@@ -169,11 +251,12 @@ read per snapshot (`getCapabilitySnapshot` returns `{ availability, commerce }`,
|
|
|
169
251
|
three-second timeout, shape validated): a stalled, failing, or invalid
|
|
170
252
|
provider yields a degraded block (`createDegradedCommerceCapabilities`:
|
|
171
253
|
`availability: 'temporarily-unavailable'`, nothing sold) and never rejects the
|
|
172
|
-
capability read.
|
|
173
|
-
after the request returns, since the store UI
|
|
174
|
-
it when the provider reports that
|
|
175
|
-
`
|
|
176
|
-
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
|
|
177
260
|
snapshot: the provider reads its own `availability` and fails closed before any
|
|
178
261
|
store UI opens. A `commerce.v2.*` request that reaches a transport without a
|
|
179
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;
|
|
@@ -45,11 +55,32 @@ export interface CreateBridgeCommerceProviderInput {
|
|
|
45
55
|
/** Sends one validated bridge request; the gateway's `request` fits. */
|
|
46
56
|
readonly request: <TData>(method: BridgeCommerceV2Method, payload: unknown) => Promise<TData>;
|
|
47
57
|
readonly subscribe: (listener: (event: BridgeCommerceEvent) => void) => () => void;
|
|
58
|
+
/**
|
|
59
|
+
* The bridge host accepts `FinishTransactionInput.retry` (it is built on the
|
|
60
|
+
* same contract and its provider declares `acceptsFinishRetry`). Absent, the
|
|
61
|
+
* provider does not declare it and callers never send `retry`.
|
|
62
|
+
*/
|
|
63
|
+
readonly acceptsFinishRetry?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The host's purchase presentation facts
|
|
66
|
+
* (`CommerceProviderBridge.subscribePresentation`), each fact of a call
|
|
67
|
+
* that opened no sheet delivered before that call's answer. Only with it
|
|
68
|
+
* does this provider offer `presentation`: a host built before the
|
|
69
|
+
* protocol, or one whose provider has no facts, leaves it out, and the
|
|
70
|
+
* consumer keeps its behavior without them.
|
|
71
|
+
*/
|
|
72
|
+
readonly subscribePresentation?: (listener: (event: BridgeCommercePresentationEvent) => void) => () => void;
|
|
48
73
|
}
|
|
49
74
|
/**
|
|
50
75
|
* `CommerceProvider` over a bridge that serves the `commerce.v2.*` methods and
|
|
51
76
|
* the event envelope. Results are validated again on this side, so a transport
|
|
52
77
|
* that forgets to is still fail-closed; bridge errors become `CommerceError`s
|
|
53
78
|
* with the bridge code kept as `storeCode` when it is not already a v2 code.
|
|
79
|
+
*
|
|
80
|
+
* With `subscribePresentation`, it offers `presentation`: the host's facts,
|
|
81
|
+
* validated and numbered again on this side, plus `not-started`, delivered
|
|
82
|
+
* before the answer, for a `requestPurchase` that settles without any fact
|
|
83
|
+
* (the bridge refused it before the host's provider ran). Every call it
|
|
84
|
+
* forwards then ends with a fact.
|
|
54
85
|
*/
|
|
55
86
|
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,8 +238,9 @@ export function createBridgeCommerceProvider(input) {
|
|
|
184
238
|
});
|
|
185
239
|
}
|
|
186
240
|
};
|
|
187
|
-
|
|
241
|
+
const provider = {
|
|
188
242
|
store: input.store,
|
|
243
|
+
...(input.acceptsFinishRetry === true ? { acceptsFinishRetry: true } : {}),
|
|
189
244
|
async getCapabilities() {
|
|
190
245
|
return (await send('commerce.v2.getCapabilities', { protocol: commerceProtocol }))
|
|
191
246
|
.capabilities;
|
|
@@ -231,6 +286,43 @@ export function createBridgeCommerceProvider(input) {
|
|
|
231
286
|
});
|
|
232
287
|
},
|
|
233
288
|
};
|
|
289
|
+
const subscribePresentation = input.subscribePresentation;
|
|
290
|
+
return subscribePresentation === undefined
|
|
291
|
+
? provider
|
|
292
|
+
: withBridgePresentation(provider, subscribePresentation);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* `presentation` over the host's facts: validated, then relayed by the
|
|
296
|
+
* platform's `createPurchasePresentationRelay` (a superseded fact dropped,
|
|
297
|
+
* the rest numbered again, `not-started` added for a call that settled
|
|
298
|
+
* without a fact). One attachment to the host serves every listener.
|
|
299
|
+
*/
|
|
300
|
+
function withBridgePresentation(provider, subscribePresentation) {
|
|
301
|
+
const relay = createPurchasePresentationRelay({
|
|
302
|
+
subscribe: (listener) => subscribePresentation((envelope) => {
|
|
303
|
+
if (namesOtherProtocol(envelope, bridgeCommercePresentationEventProtocol)) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
let event;
|
|
307
|
+
try {
|
|
308
|
+
event = assertCommercePresentationEventEnvelope(envelope).event;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
console.warn('The bridge delivered an invalid purchase presentation fact.', error);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
listener(event);
|
|
315
|
+
}),
|
|
316
|
+
}, {
|
|
317
|
+
onListenerError: (error) => {
|
|
318
|
+
console.warn('A purchase presentation listener failed.', error);
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
...provider,
|
|
323
|
+
presentation: relay.presentation,
|
|
324
|
+
requestPurchase: (request) => relay.track(request.idempotencyKey, () => provider.requestPurchase(request)),
|
|
325
|
+
};
|
|
234
326
|
}
|
|
235
327
|
function toCommerceError(error) {
|
|
236
328
|
if (error instanceof CommerceError) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ 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
|
-
export { createStoreCommerceProvider, recoverableErrorCodes, type CreateStoreCommerceProviderInput, type StoreCommerceCapabilityOverrides, type StoreCommerceProductMapping, type StoreCommerceProvider, } from './store-commerce-provider.js';
|
|
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
10
|
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
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';
|
|
@@ -13,9 +14,17 @@ export { createCapacitorViewport, type CapacitorViewportBaseState, type Capacito
|
|
|
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,15 +4,17 @@ 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
|
-
export { createStoreCommerceProvider, recoverableErrorCodes, } from './store-commerce-provider.js';
|
|
17
|
+
export { accountBindingLookupFailedStoreCode, createStoreCommerceProvider, purchaseGateFailedStoreCode, purchaseGateTimeoutMs, purchasesPausedStoreCode, recoverableErrorCodes, } from './store-commerce-provider.js';
|
|
16
18
|
export { defaultStoreSdkErrorCodeMap, normalizeStoreSdkError, } from './store-sdk.js';
|
|
17
19
|
export { createCapacitorAppEvents, } from './app-events.js';
|
|
18
20
|
export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, } from './native-http.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,10 +69,13 @@ 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,
|
|
77
|
+
// The host below is this package's, on the same contract.
|
|
78
|
+
acceptsFinishRetry: commerceProvider.commerce.acceptsFinishRetry === true,
|
|
66
79
|
request: (method, payload) => request(method, payload),
|
|
67
80
|
// The provider's channel (`NativeBridge`) may carry other protocols.
|
|
68
81
|
// Only an envelope that names another protocol is dropped; anything
|
|
@@ -73,6 +86,10 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
73
86
|
listener(envelope);
|
|
74
87
|
}
|
|
75
88
|
}),
|
|
89
|
+
// Facts only from a bridge that serves them (`createCommerceServiceProvider`
|
|
90
|
+
// over a provider with `presentation`); otherwise `commerce` has no
|
|
91
|
+
// `presentation` and purchases keep the result-based pause below.
|
|
92
|
+
...(subscribePresentation === undefined ? {} : { subscribePresentation }),
|
|
76
93
|
});
|
|
77
94
|
const notificationsProvider = providers.notifications;
|
|
78
95
|
// Served through the same request path as every other provider method, as
|
|
@@ -135,6 +152,22 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
135
152
|
? lifecycle.beginExternalActivity?.()
|
|
136
153
|
: undefined;
|
|
137
154
|
let holdActivity = false;
|
|
155
|
+
// Watched before the request is sent: a provider reports a sheet that
|
|
156
|
+
// never opened before it answers.
|
|
157
|
+
const presentation = releaseExternalActivity !== undefined
|
|
158
|
+
&& method === 'commerce.v2.requestPurchase'
|
|
159
|
+
? watchPurchasePresentation(payload)
|
|
160
|
+
: undefined;
|
|
161
|
+
// Released once: by the sheet's own end (which may come while the call
|
|
162
|
+
// still runs, for example a safety-timeout `closed`), or after the call.
|
|
163
|
+
let released = false;
|
|
164
|
+
const release = () => {
|
|
165
|
+
if (!released) {
|
|
166
|
+
released = true;
|
|
167
|
+
releaseExternalActivity?.();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
presentation?.onEnd(release);
|
|
138
171
|
try {
|
|
139
172
|
const provider = leaderboardRoute === 'remote'
|
|
140
173
|
&& (method === 'leaderboard.submitScore' || method === 'leaderboard.open')
|
|
@@ -146,11 +179,15 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
146
179
|
const data = await sendRequest(method, payload, provider);
|
|
147
180
|
if (releaseExternalActivity !== undefined
|
|
148
181
|
&& method === 'commerce.v2.requestPurchase'
|
|
149
|
-
&& isRecord(data) && data.outcome === 'launched'
|
|
150
|
-
|
|
151
|
-
//
|
|
182
|
+
&& isRecord(data) && data.outcome === 'launched'
|
|
183
|
+
&& presentation?.reported() !== true) {
|
|
184
|
+
// No sheet facts for this purchase: the store UI may still be
|
|
185
|
+
// open, so stay paused until this product's purchase event
|
|
186
|
+
// arrives or the safety timeout ends. With facts, the sheet's
|
|
187
|
+
// own `closed` ends the pause (below), and a pending purchase
|
|
188
|
+
// whose sheet closed holds nothing.
|
|
152
189
|
holdActivity = true;
|
|
153
|
-
holdLaunchedPurchaseActivity(
|
|
190
|
+
holdLaunchedPurchaseActivity(release, isRecord(payload) ? payload.logicalId : undefined);
|
|
154
191
|
}
|
|
155
192
|
return data;
|
|
156
193
|
}
|
|
@@ -166,10 +203,106 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
166
203
|
return await sendRequest(method, payload, undefined);
|
|
167
204
|
}
|
|
168
205
|
finally {
|
|
169
|
-
if (
|
|
170
|
-
|
|
206
|
+
if (holdActivity) {
|
|
207
|
+
presentation?.dispose();
|
|
208
|
+
}
|
|
209
|
+
else if (!released && presentation?.open() === true) {
|
|
210
|
+
// The sheet is still up, whatever the answer (a result, a pending
|
|
211
|
+
// purchase, or a throw): stay paused until its `closed` fact.
|
|
212
|
+
holdPresentedPurchaseActivity(release, presentation);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
presentation?.dispose();
|
|
216
|
+
release();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Follows one purchase's presentation facts (by its idempotency key) from
|
|
222
|
+
* `commerce.presentation`: the bridge's facts already validated, with a
|
|
223
|
+
* superseded or replayed one dropped, so a stale fact cannot end the hold
|
|
224
|
+
* while the sheet is up. Undefined when the bridge serves no facts or the
|
|
225
|
+
* payload has no key.
|
|
226
|
+
*/
|
|
227
|
+
function watchPurchasePresentation(payload) {
|
|
228
|
+
const idempotencyKey = isRecord(payload) ? payload.idempotencyKey : undefined;
|
|
229
|
+
const facts = commerce?.presentation;
|
|
230
|
+
if (facts === undefined || typeof idempotencyKey !== 'string') {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
// `held`: the sheet is (`open`) or may be (`unknown`) up; `opened`: seen open.
|
|
234
|
+
let held = false;
|
|
235
|
+
let opened = false;
|
|
236
|
+
let ended = false;
|
|
237
|
+
let onEnd;
|
|
238
|
+
let unsubscribe;
|
|
239
|
+
let disposed = false;
|
|
240
|
+
const dispose = () => {
|
|
241
|
+
disposed = true;
|
|
242
|
+
onEnd = undefined;
|
|
243
|
+
unsubscribe?.();
|
|
244
|
+
unsubscribe = undefined;
|
|
245
|
+
};
|
|
246
|
+
const subscription = facts.subscribe((event) => {
|
|
247
|
+
if (event.idempotencyKey !== idempotencyKey || ended) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const { state } = event;
|
|
251
|
+
if (state === 'open' || state === 'unknown') {
|
|
252
|
+
// `unknown`: the sheet may still be up; hold as for an open one.
|
|
253
|
+
held = true;
|
|
254
|
+
opened ||= state === 'open';
|
|
171
255
|
}
|
|
256
|
+
else if (state === 'closed' || (state === 'not-started' && !opened)) {
|
|
257
|
+
// A sheet seen open ends only with `closed` (its provider turns a
|
|
258
|
+
// later `not-started` into `closed`); a `not-started` then belongs
|
|
259
|
+
// to another call of the same key.
|
|
260
|
+
ended = true;
|
|
261
|
+
const end = onEnd;
|
|
262
|
+
dispose();
|
|
263
|
+
end?.();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
if (disposed) {
|
|
267
|
+
subscription();
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
unsubscribe = subscription;
|
|
172
271
|
}
|
|
272
|
+
return {
|
|
273
|
+
reported: () => held || ended,
|
|
274
|
+
open: () => held && !ended,
|
|
275
|
+
onEnd(callback) {
|
|
276
|
+
if (ended) {
|
|
277
|
+
callback();
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
onEnd = callback;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
dispose,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Keeps a purchase's external activity open while its store sheet is up:
|
|
288
|
+
* until the sheet's `closed` fact, the safety timeout, or dispose. A
|
|
289
|
+
* result, a pending purchase, or a purchase event does not end it.
|
|
290
|
+
*/
|
|
291
|
+
function holdPresentedPurchaseActivity(release, presentation) {
|
|
292
|
+
let done = false;
|
|
293
|
+
const finish = () => {
|
|
294
|
+
if (done) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
done = true;
|
|
298
|
+
launchedPurchaseHolds.delete(finish);
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
presentation.dispose();
|
|
301
|
+
release();
|
|
302
|
+
};
|
|
303
|
+
launchedPurchaseHolds.add(finish);
|
|
304
|
+
const timer = setTimeout(finish, presentedPurchaseActivityTimeoutMs);
|
|
305
|
+
presentation.onEnd(finish);
|
|
173
306
|
}
|
|
174
307
|
/**
|
|
175
308
|
* 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;
|