@mpgd/adapter-capacitor 0.16.3 → 0.17.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 +41 -0
- package/dist/commerce-bridge.d.ts +6 -0
- package/dist/commerce-bridge.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/store-commerce-provider.d.ts +51 -2
- package/dist/store-commerce-provider.js +125 -4
- package/dist/store-sdk.d.ts +2 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -133,6 +133,47 @@ 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. The reconciler
|
|
152
|
+
journals a refused attempt as terminal `failed` even when it is retryable:
|
|
153
|
+
only trusted presentation facts (`not-started`) keep a key reusable, and the
|
|
154
|
+
Capacitor bridge does not carry them yet, so a later try uses a new
|
|
155
|
+
idempotency key. The store provider deliberately exposes no `presentation`
|
|
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
|
|
159
|
+
purchase's external activity (the game is paused, with no sheet shown), so it
|
|
160
|
+
should answer from a flag the game already holds rather than a slow network
|
|
161
|
+
call; the timeout bounds that pause. Recovery, restore, finishing, events, `fetchProducts`, and
|
|
162
|
+
`getAvailablePurchases` never
|
|
163
|
+
ask the gate, so purchases already paid for keep reconciling while new sales
|
|
164
|
+
are refused. A failed or missing account binding is a `failed` result too,
|
|
165
|
+
not a throw: a thrown `CommerceError` keeps its code (with the same exception
|
|
166
|
+
for launched or cancelled codes), and anything else is
|
|
167
|
+
`ACCOUNT_BINDING_REQUIRED` with `ACCOUNT_BINDING_LOOKUP_FAILED`, retryable.
|
|
168
|
+
Over the bridge a thrown error keeps only `code`, `message`, and
|
|
169
|
+
`retryable`, while a result keeps `storeCode`, so the game can tell
|
|
170
|
+
`PURCHASES_PAUSED` from `PLAY_BILLING_ACCOUNT_LOOKUP_FAILED`. The gate is not
|
|
171
|
+
reflected in `getCapabilities()`: `availability` describes the store and
|
|
172
|
+
gates the reconciler (a paused gate there would read as `STORE_UNAVAILABLE`
|
|
173
|
+
and lose the reason), and a capability read stays free of server round trips.
|
|
174
|
+
A shop hides its buy buttons from the same server flag it hands the gate,
|
|
175
|
+
for example the sales flag of its account binding response.
|
|
176
|
+
|
|
136
177
|
Two failure classes are not terminal. When `requestPurchase` throws or
|
|
137
178
|
returns a code that normalizes to `OUTCOME_UNKNOWN` (for example
|
|
138
179
|
`PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`,
|
|
@@ -45,6 +45,12 @@ export interface CreateBridgeCommerceProviderInput {
|
|
|
45
45
|
/** Sends one validated bridge request; the gateway's `request` fits. */
|
|
46
46
|
readonly request: <TData>(method: BridgeCommerceV2Method, payload: unknown) => Promise<TData>;
|
|
47
47
|
readonly subscribe: (listener: (event: BridgeCommerceEvent) => void) => () => void;
|
|
48
|
+
/**
|
|
49
|
+
* The bridge host accepts `FinishTransactionInput.retry` (it is built on the
|
|
50
|
+
* same contract and its provider declares `acceptsFinishRetry`). Absent, the
|
|
51
|
+
* provider does not declare it and callers never send `retry`.
|
|
52
|
+
*/
|
|
53
|
+
readonly acceptsFinishRetry?: boolean;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* `CommerceProvider` over a bridge that serves the `commerce.v2.*` methods and
|
package/dist/commerce-bridge.js
CHANGED
|
@@ -186,6 +186,7 @@ export function createBridgeCommerceProvider(input) {
|
|
|
186
186
|
};
|
|
187
187
|
return {
|
|
188
188
|
store: input.store,
|
|
189
|
+
...(input.acceptsFinishRetry === true ? { acceptsFinishRetry: true } : {}),
|
|
189
190
|
async getCapabilities() {
|
|
190
191
|
return (await send('commerce.v2.getCapabilities', { protocol: commerceProtocol }))
|
|
191
192
|
.capabilities;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ 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
6
|
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, createDegradedNotificationCapabilities, type CapacitorCommerceServiceProvider, type CapacitorNotificationServiceProvider, type CapacitorNotificationsV2Service, type CapacitorProviderCapabilitySnapshot, type CapacitorProviderRegistry, type CapacitorServiceProvider, type NativeBridge, } from './providers.js';
|
|
7
7
|
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';
|
|
8
|
+
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
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
10
|
export { createCapacitorAppEvents, type CapacitorAppEventsApi, type CapacitorIncomingUrlKind, type CapacitorVisibilitySource, } from './app-events.js';
|
|
11
11
|
export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, type CapacitorJsonRequest, type CapacitorJsonResponse, type CapacitorNativeHttpErrorCode, type CapacitorNativeJsonTransport, type CreateCapacitorNativeJsonTransportInput, } from './native-http.js';
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { createCapacitorViewport } from './viewport.js';
|
|
|
12
12
|
export { capacitorCrashlyticsPluginName, capacitorCrashReportingProvider, createCapacitorCrashReporter, createLocalCrashCollectionPreferenceStore, } from './crash-reporting.js';
|
|
13
13
|
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, createDegradedNotificationCapabilities, } from './providers.js';
|
|
14
14
|
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, } from './commerce-bridge.js';
|
|
15
|
-
export { createStoreCommerceProvider, recoverableErrorCodes, } from './store-commerce-provider.js';
|
|
15
|
+
export { accountBindingLookupFailedStoreCode, createStoreCommerceProvider, purchaseGateFailedStoreCode, purchaseGateTimeoutMs, purchasesPausedStoreCode, recoverableErrorCodes, } from './store-commerce-provider.js';
|
|
16
16
|
export { defaultStoreSdkErrorCodeMap, normalizeStoreSdkError, } from './store-sdk.js';
|
|
17
17
|
export { createCapacitorAppEvents, } from './app-events.js';
|
|
18
18
|
export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, } from './native-http.js';
|
|
@@ -63,6 +63,8 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
63
63
|
? undefined
|
|
64
64
|
: createBridgeCommerceProvider({
|
|
65
65
|
store: commerceProvider.commerce.store,
|
|
66
|
+
// The host below is this package's, on the same contract.
|
|
67
|
+
acceptsFinishRetry: commerceProvider.commerce.acceptsFinishRetry === true,
|
|
66
68
|
request: (method, payload) => request(method, payload),
|
|
67
69
|
// The provider's channel (`NativeBridge`) may carry other protocols.
|
|
68
70
|
// Only an envelope that names another protocol is dropped; anything
|
|
@@ -1,5 +1,5 @@
|
|
|
1
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';
|
|
2
|
+
import { type CommerceAuthorityCapabilities, type CommerceCatalogCapabilities, type CommerceErrorCode, type CommerceLifecycleCapabilities, type CommerceLogicalProductId, type CommerceProvider, type CommercePurchaseCapabilities, type PurchaseKind, type PurchaseSource, type StoreId, type StorePurchaseAccountBinding } from '@mpgd/platform/commerce';
|
|
3
3
|
import { type StoreSdk, type StoreSdkErrorCodeMap, type StoreSdkPurchase } from './store-sdk.js';
|
|
4
4
|
/** One catalog product sold through the store under `storeSku`. */
|
|
5
5
|
export interface StoreCommerceProductMapping {
|
|
@@ -15,6 +15,41 @@ export interface StoreCommerceCapabilityOverrides {
|
|
|
15
15
|
readonly catalog?: Partial<CommerceCatalogCapabilities>;
|
|
16
16
|
readonly lifecycle?: Partial<CommerceLifecycleCapabilities>;
|
|
17
17
|
}
|
|
18
|
+
/** What a purchase gate is asked about: one new purchase, before any store UI opens. */
|
|
19
|
+
export interface StorePurchaseGateRequest {
|
|
20
|
+
readonly store: StoreId;
|
|
21
|
+
readonly logicalId: CommerceLogicalProductId;
|
|
22
|
+
readonly storeSku: string;
|
|
23
|
+
readonly kind: PurchaseKind;
|
|
24
|
+
readonly source: PurchaseSource;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A purchase gate's answer. A refusal becomes a `failed` purchase result with
|
|
28
|
+
* `ITEM_UNAVAILABLE` and `storeCode` (default `PURCHASES_PAUSED`), returned as
|
|
29
|
+
* data so the reason survives the bridge; `retryable` defaults to false.
|
|
30
|
+
*/
|
|
31
|
+
export type StorePurchaseGateDecision = {
|
|
32
|
+
readonly allowed: true;
|
|
33
|
+
} | {
|
|
34
|
+
readonly allowed: false;
|
|
35
|
+
readonly storeCode?: string;
|
|
36
|
+
readonly message?: string;
|
|
37
|
+
readonly retryable?: boolean;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The game's sales switch: asked before a NEW purchase opens the store UI,
|
|
41
|
+
* after the product, availability, and kind checks and before the account
|
|
42
|
+
* binding. Recovery, restore, finishing, events, and `getAvailablePurchases`
|
|
43
|
+
* never consult it, so purchases already paid for keep reconciling while new
|
|
44
|
+
* sales are refused. A gate that throws refuses the purchase (fail closed).
|
|
45
|
+
*/
|
|
46
|
+
export type StorePurchaseGate = (request: StorePurchaseGateRequest) => StorePurchaseGateDecision | Promise<StorePurchaseGateDecision>;
|
|
47
|
+
/** `storeCode` of a purchase the gate refused without naming its own. */
|
|
48
|
+
export declare const purchasesPausedStoreCode: 'PURCHASES_PAUSED';
|
|
49
|
+
/** `storeCode` of a purchase refused because the gate threw or answered nonsense. */
|
|
50
|
+
export declare const purchaseGateFailedStoreCode: 'PURCHASE_GATE_FAILED';
|
|
51
|
+
/** `storeCode` of a purchase refused because the account binding lookup threw. */
|
|
52
|
+
export declare const accountBindingLookupFailedStoreCode: 'ACCOUNT_BINDING_LOOKUP_FAILED';
|
|
18
53
|
export interface CreateStoreCommerceProviderInput {
|
|
19
54
|
readonly sdk: StoreSdk;
|
|
20
55
|
/** Defaults to `sdk.store`. */
|
|
@@ -27,8 +62,15 @@ export interface CreateStoreCommerceProviderInput {
|
|
|
27
62
|
* and SKU; a store provider replaces it to match its backend verifier.
|
|
28
63
|
*/
|
|
29
64
|
readonly evidencePayload?: (purchase: StoreSdkPurchase) => PlatformEvidenceEnvelope['payload'];
|
|
30
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Resolves the player's store account binding before a purchase opens. A
|
|
67
|
+
* failure (thrown, or no binding while `authority.accountBinding` is
|
|
68
|
+
* `required`) is answered as a `failed` purchase result, not thrown, so its
|
|
69
|
+
* `storeCode` survives the bridge.
|
|
70
|
+
*/
|
|
31
71
|
readonly accountBinding?: () => Promise<StorePurchaseAccountBinding | undefined>;
|
|
72
|
+
/** Optional sales switch for new purchases; see `StorePurchaseGate`. */
|
|
73
|
+
readonly purchaseGate?: StorePurchaseGate;
|
|
32
74
|
readonly capabilities?: StoreCommerceCapabilityOverrides;
|
|
33
75
|
readonly authority: CommerceAuthorityCapabilities;
|
|
34
76
|
/** Per-store error codes merged over `defaultStoreSdkErrorCodeMap`. */
|
|
@@ -39,6 +81,13 @@ export interface StoreCommerceProvider extends CommerceProvider {
|
|
|
39
81
|
dispose(): void;
|
|
40
82
|
}
|
|
41
83
|
/**
|
|
84
|
+
* Upper bound on one purchase gate answer. The gate runs while the gateway
|
|
85
|
+
* holds the purchase's external activity, so a gate that never settles would
|
|
86
|
+
* keep the game paused; past this bound the purchase is refused
|
|
87
|
+
* (`PURCHASE_GATE_FAILED`, retryable) and a late answer is ignored.
|
|
88
|
+
*/
|
|
89
|
+
export declare const purchaseGateTimeoutMs = 3000;
|
|
90
|
+
/**
|
|
42
91
|
* `CommerceProvider` over an in-process `StoreSdk`. The provider maps logical
|
|
43
92
|
* ids to store SKUs both ways, composes `CommerceCapabilities` from the SDK
|
|
44
93
|
* declaration, builds `StorePurchase` evidence, buffers SDK events until the
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { assertFinishTransactionInput, assertFinishTransactionResult, assertRequestPurchaseSupported, assertStoreProduct, assertStorePurchase, CommerceError, createCommerceEventBuffer, } from '@mpgd/platform/commerce';
|
|
2
2
|
import { defaultStoreSdkErrorCodeMap, normalizeStoreSdkError, } from './store-sdk.js';
|
|
3
|
+
/** `storeCode` of a purchase the gate refused without naming its own. */
|
|
4
|
+
export const purchasesPausedStoreCode = 'PURCHASES_PAUSED';
|
|
5
|
+
/** `storeCode` of a purchase refused because the gate threw or answered nonsense. */
|
|
6
|
+
export const purchaseGateFailedStoreCode = 'PURCHASE_GATE_FAILED';
|
|
7
|
+
/** `storeCode` of a purchase refused because the account binding lookup threw. */
|
|
8
|
+
export const accountBindingLookupFailedStoreCode = 'ACCOUNT_BINDING_LOOKUP_FAILED';
|
|
3
9
|
const availabilityTimeoutMs = 3_000;
|
|
10
|
+
/**
|
|
11
|
+
* Upper bound on one purchase gate answer. The gate runs while the gateway
|
|
12
|
+
* holds the purchase's external activity, so a gate that never settles would
|
|
13
|
+
* keep the game paused; past this bound the purchase is refused
|
|
14
|
+
* (`PURCHASE_GATE_FAILED`, retryable) and a late answer is ignored.
|
|
15
|
+
*/
|
|
16
|
+
export const purchaseGateTimeoutMs = 3_000;
|
|
4
17
|
const availabilitySet = new Set([
|
|
5
18
|
'unsupported',
|
|
6
19
|
'configuration-required',
|
|
@@ -176,6 +189,8 @@ export function createStoreCommerceProvider(input) {
|
|
|
176
189
|
const restorePurchases = sdk.restorePurchases;
|
|
177
190
|
const provider = {
|
|
178
191
|
store,
|
|
192
|
+
// `retry` is forwarded to the SDK below.
|
|
193
|
+
...(finishTransaction === undefined ? {} : { acceptsFinishRetry: true }),
|
|
179
194
|
getCapabilities,
|
|
180
195
|
async fetchProducts(request = {}) {
|
|
181
196
|
const type = request.type ?? 'all';
|
|
@@ -245,11 +260,34 @@ export function createStoreCommerceProvider(input) {
|
|
|
245
260
|
const capabilities = await getCapabilities();
|
|
246
261
|
assertAvailable(capabilities.availability, store);
|
|
247
262
|
assertRequestPurchaseSupported(capabilities, request, mapping.kind);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
// Pre-launch refusals below are data, not throws: a `failed` result
|
|
264
|
+
// keeps its `storeCode` across the bridge, a thrown error does not.
|
|
265
|
+
if (input.purchaseGate !== undefined) {
|
|
266
|
+
const refusal = await askPurchaseGate(input.purchaseGate, {
|
|
267
|
+
store,
|
|
268
|
+
logicalId: mapping.logicalId,
|
|
269
|
+
storeSku: mapping.storeSku,
|
|
270
|
+
kind: mapping.kind,
|
|
271
|
+
source: request.source,
|
|
252
272
|
});
|
|
273
|
+
if (refusal !== undefined) {
|
|
274
|
+
return { outcome: 'failed', error: refusal };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let accountBinding;
|
|
278
|
+
try {
|
|
279
|
+
accountBinding = await input.accountBinding?.();
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
return { outcome: 'failed', error: accountBindingFailure(error, store) };
|
|
283
|
+
}
|
|
284
|
+
if (input.authority.accountBinding === 'required' && accountBinding === undefined) {
|
|
285
|
+
return {
|
|
286
|
+
outcome: 'failed',
|
|
287
|
+
error: new CommerceError('ACCOUNT_BINDING_REQUIRED', {
|
|
288
|
+
message: `Store ${store} requires an account binding before a purchase.`,
|
|
289
|
+
}).toCommerceErrorInfo(),
|
|
290
|
+
};
|
|
253
291
|
}
|
|
254
292
|
const offerId = request.offerId ?? mapping.offerId;
|
|
255
293
|
let result;
|
|
@@ -322,6 +360,7 @@ export function createStoreCommerceProvider(input) {
|
|
|
322
360
|
transactionId: finish.transactionId,
|
|
323
361
|
isConsumable: finish.isConsumable,
|
|
324
362
|
ledgerEntryId: finish.ledgerEntryId,
|
|
363
|
+
...(finish.retry === true ? { retry: true } : {}),
|
|
325
364
|
});
|
|
326
365
|
}
|
|
327
366
|
catch (error) {
|
|
@@ -387,6 +426,88 @@ function defaultEvidencePayload(purchase) {
|
|
|
387
426
|
: { originalTransactionId: purchase.originalTransactionId }),
|
|
388
427
|
};
|
|
389
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Codes a pre-launch hook (the purchase gate or the account binding) must not
|
|
431
|
+
* report: they claim a store sheet was shown or may hold a purchase, and
|
|
432
|
+
* nothing was launched. A hook that throws one is reported under the hook's
|
|
433
|
+
* own refusal code instead.
|
|
434
|
+
*/
|
|
435
|
+
function misleadsBeforeLaunch(code) {
|
|
436
|
+
return recoverableErrorCodes.has(code) || code === 'USER_CANCELLED';
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Asks the gate about one new purchase, bounded by `purchaseGateTimeoutMs`;
|
|
440
|
+
* undefined means allowed. A refusal
|
|
441
|
+
* (`allowed: false`) is the gate's definitive answer: `PURCHASES_PAUSED` or
|
|
442
|
+
* its own code, retryable only when it says so. A gate that could not answer
|
|
443
|
+
* (it threw, timed out, or returned anything malformed) is `PURCHASE_GATE_FAILED`, and
|
|
444
|
+
* always retryable: the sales flag is unknown, not closed, so asking again
|
|
445
|
+
* may succeed. A thrown `CommerceError` keeps its code unless the code
|
|
446
|
+
* claims a launched or cancelled sheet.
|
|
447
|
+
*/
|
|
448
|
+
async function askPurchaseGate(gate, request) {
|
|
449
|
+
const gateFailed = (message, cause) => new CommerceError('ITEM_UNAVAILABLE', {
|
|
450
|
+
message,
|
|
451
|
+
storeCode: purchaseGateFailedStoreCode,
|
|
452
|
+
retryable: true,
|
|
453
|
+
...(cause === undefined ? {} : { cause }),
|
|
454
|
+
}).toCommerceErrorInfo();
|
|
455
|
+
let decision;
|
|
456
|
+
let timer;
|
|
457
|
+
try {
|
|
458
|
+
decision = await Promise.race([
|
|
459
|
+
Promise.resolve().then(() => gate(request)),
|
|
460
|
+
new Promise((_resolve, reject) => {
|
|
461
|
+
timer = setTimeout(() => reject(new Error(`The purchase gate did not answer within ${purchaseGateTimeoutMs} ms.`)), purchaseGateTimeoutMs);
|
|
462
|
+
}),
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (error instanceof CommerceError && !misleadsBeforeLaunch(error.code)) {
|
|
467
|
+
return error.toCommerceErrorInfo();
|
|
468
|
+
}
|
|
469
|
+
return gateFailed(`The purchase gate of ${request.store} failed; ${request.logicalId} is not sold now.`, error);
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
if (timer !== undefined) {
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (isRecord(decision) && decision.allowed === true) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
if (!isRecord(decision) || decision.allowed !== false) {
|
|
480
|
+
return gateFailed(`The purchase gate of ${request.store} returned an invalid decision.`);
|
|
481
|
+
}
|
|
482
|
+
const storeCode = typeof decision.storeCode === 'string' && decision.storeCode.length > 0
|
|
483
|
+
? decision.storeCode
|
|
484
|
+
: purchasesPausedStoreCode;
|
|
485
|
+
return new CommerceError('ITEM_UNAVAILABLE', {
|
|
486
|
+
message: typeof decision.message === 'string' && decision.message.length > 0
|
|
487
|
+
? decision.message
|
|
488
|
+
: `New purchases of ${request.logicalId} are paused.`,
|
|
489
|
+
storeCode,
|
|
490
|
+
retryable: decision.retryable === true,
|
|
491
|
+
}).toCommerceErrorInfo();
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* A failed account binding lookup. A `CommerceError` keeps its code (a store
|
|
495
|
+
* provider names its own lookup failure) unless the code claims a launched or
|
|
496
|
+
* cancelled sheet; anything else is `ACCOUNT_BINDING_REQUIRED` with
|
|
497
|
+
* `ACCOUNT_BINDING_LOOKUP_FAILED`, retryable, so every store reports the
|
|
498
|
+
* same reason.
|
|
499
|
+
*/
|
|
500
|
+
function accountBindingFailure(error, store) {
|
|
501
|
+
if (error instanceof CommerceError && !misleadsBeforeLaunch(error.code)) {
|
|
502
|
+
return error.toCommerceErrorInfo();
|
|
503
|
+
}
|
|
504
|
+
return new CommerceError('ACCOUNT_BINDING_REQUIRED', {
|
|
505
|
+
message: `Store ${store} could not resolve the account binding.`,
|
|
506
|
+
storeCode: accountBindingLookupFailedStoreCode,
|
|
507
|
+
retryable: true,
|
|
508
|
+
cause: error,
|
|
509
|
+
}).toCommerceErrorInfo();
|
|
510
|
+
}
|
|
390
511
|
function assertAvailable(availability, store) {
|
|
391
512
|
switch (availability) {
|
|
392
513
|
case 'available':
|
package/dist/store-sdk.d.ts
CHANGED
|
@@ -91,6 +91,8 @@ export interface StoreSdkPurchase {
|
|
|
91
91
|
export interface StoreSdkFinishTransactionInput {
|
|
92
92
|
readonly transactionId: string;
|
|
93
93
|
readonly isConsumable: boolean;
|
|
94
|
+
/** `FinishTransactionInput.retry`: an earlier finish of this purchase was asked for. */
|
|
95
|
+
readonly retry?: boolean;
|
|
94
96
|
/** The ledger entry that authorized the finish; a plugin may record it. */
|
|
95
97
|
readonly ledgerEntryId: string;
|
|
96
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-capacitor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Capacitor platform adapter for mpgd Android and iOS shells.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@capacitor/app": "8.1.1",
|
|
54
|
-
"@mpgd/capacitor-game-services": "0.9.
|
|
55
|
-
"@mpgd/
|
|
56
|
-
"@mpgd/
|
|
57
|
-
"@mpgd/bridge": "0.
|
|
58
|
-
"@mpgd/target-config": "0.24.
|
|
54
|
+
"@mpgd/capacitor-game-services": "0.9.4",
|
|
55
|
+
"@mpgd/game-services": "0.31.0",
|
|
56
|
+
"@mpgd/platform": "0.33.0",
|
|
57
|
+
"@mpgd/bridge": "0.17.0",
|
|
58
|
+
"@mpgd/target-config": "0.24.4"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@capacitor-community/admob": "^8.1.0",
|
|
@@ -89,10 +89,10 @@
|
|
|
89
89
|
"@capacitor/core": "8.5.2",
|
|
90
90
|
"@capacitor/local-notifications": "8.3.1",
|
|
91
91
|
"@capawesome/capacitor-app-review": "8.1.0",
|
|
92
|
-
"ttsc": "0.
|
|
92
|
+
"ttsc": "0.34.2",
|
|
93
93
|
"typescript": "7.0.2",
|
|
94
94
|
"vitest": "^4.0.0",
|
|
95
|
-
"@mpgd/game-runtime": "0.8.
|
|
95
|
+
"@mpgd/game-runtime": "0.8.10"
|
|
96
96
|
},
|
|
97
97
|
"main": "./dist/index.js",
|
|
98
98
|
"types": "./dist/index.d.ts",
|