@mpgd/adapter-capacitor 0.7.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -9
- package/dist/app-store-server.d.ts +12 -12
- package/dist/app-store-server.js +10 -119
- package/dist/commerce-bridge.d.ts +59 -0
- package/dist/commerce-bridge.js +265 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +96 -6
- package/dist/providers.d.ts +52 -2
- package/dist/providers.js +158 -33
- package/dist/store-commerce-provider.d.ts +55 -0
- package/dist/store-commerce-provider.js +437 -0
- package/dist/store-sdk.d.ts +131 -0
- package/dist/store-sdk.js +83 -0
- package/package.json +7 -12
package/README.md
CHANGED
|
@@ -64,17 +64,128 @@ The source tests and installed-tarball consumer validate composition, types,
|
|
|
64
64
|
error paths, and fallback behavior. They do not establish that any optional
|
|
65
65
|
SDK works on a physical device or that a store/ad setup is release-ready.
|
|
66
66
|
|
|
67
|
+
## Commerce v2 providers
|
|
68
|
+
|
|
69
|
+
Commerce v2 ([RFC](../../docs/specs/commerce/commerce-v2-rfc.md), PR B1) adds
|
|
70
|
+
a second commerce surface next to the v1 `commerce.*` bridge methods. A store
|
|
71
|
+
plugin exposes an in-process `StoreSdk`, the adapter turns it into a
|
|
72
|
+
`CommerceProvider` from `@mpgd/platform/commerce`, and the provider registry
|
|
73
|
+
serves both the six `commerce.v2.*` bridge methods and the four v1 methods
|
|
74
|
+
from it.
|
|
75
|
+
|
|
76
|
+
`StoreSdk` is shaped like the OpenIAP Client Protocol so a future Capacitor
|
|
77
|
+
IAP plugin over the OpenIAP cores is a drop-in: `fetchProducts({ skus, type })`,
|
|
78
|
+
`requestPurchase({ sku, offerId?, quantity?, accountBinding? })` answering
|
|
79
|
+
`launched`, `purchased`, `cancelled`, or `failed`, `getAvailablePurchases`
|
|
80
|
+
listing unfinished store purchases, optional `finishTransaction` and
|
|
81
|
+
`restorePurchases`, `addListener('purchaseUpdated' | 'purchaseError')`, an
|
|
82
|
+
optional `getAvailability`, and a static `capabilities` declaration (purchase
|
|
83
|
+
kinds, quantity, pending, restore, finish owner). The SDK speaks store SKUs
|
|
84
|
+
and its own error codes only. B2 adapts `@mpgd/capacitor-play-billing` and B3
|
|
85
|
+
adapts `@mpgd/capacitor-storekit` to this interface; B1 ships no plugin
|
|
86
|
+
change.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import {
|
|
90
|
+
createCapacitorPlatformGateway,
|
|
91
|
+
createCommerceServiceProvider,
|
|
92
|
+
createStoreCommerceProvider,
|
|
93
|
+
type StoreSdk,
|
|
94
|
+
} from '@mpgd/adapter-capacitor';
|
|
95
|
+
|
|
96
|
+
export function createStoreGateway(sdk: StoreSdk) {
|
|
97
|
+
const commerce = createStoreCommerceProvider({
|
|
98
|
+
sdk,
|
|
99
|
+
products: [
|
|
100
|
+
{ logicalId: 'gems_small', storeSku: 'gems.small', kind: 'consumable' },
|
|
101
|
+
{ logicalId: 'remove_ads', storeSku: 'remove.ads', kind: 'durable' },
|
|
102
|
+
],
|
|
103
|
+
evidenceSchema: 'google-play.product-purchase.v2',
|
|
104
|
+
evidencePayload: (purchase) => ({ purchaseToken: purchase.token }),
|
|
105
|
+
accountBinding: async () => ({ kind: 'obfuscated-account-id', value: 'player-1' }),
|
|
106
|
+
authority: { evidence: 'client-token', accountBinding: 'required' },
|
|
107
|
+
mapError: { PLAY_BILLING_OFFER_REQUIRED: 'CONFIGURATION_REQUIRED' },
|
|
108
|
+
});
|
|
109
|
+
return createCapacitorPlatformGateway({
|
|
110
|
+
target: 'android',
|
|
111
|
+
appVersion: '1.0.0',
|
|
112
|
+
buildId: 'game-build',
|
|
113
|
+
providers: [createCommerceServiceProvider({ id: 'play-billing', commerce })],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`createStoreCommerceProvider` maps logical ids to store SKUs both ways (an
|
|
119
|
+
unknown SKU is still reported, with `logicalId` undefined; an unknown logical
|
|
120
|
+
id fails with `ITEM_UNAVAILABLE`), rejects unsupported kinds and quantities
|
|
121
|
+
with `assertRequestPurchaseSupported` before any store UI opens, refuses to
|
|
122
|
+
open the store while `getAvailability` is not `available`, builds every
|
|
123
|
+
`StorePurchase` with the store `token` and the `{ schema: evidenceSchema,
|
|
124
|
+
payload }` envelope, buffers SDK events until the first `subscribe` with
|
|
125
|
+
`createCommerceEventBuffer`, composes `CommerceCapabilities` from the SDK
|
|
126
|
+
declaration plus `capabilities` overrides and a fresh `availability` read
|
|
127
|
+
(three-second timeout, degraded to `temporarily-unavailable`), validates every
|
|
128
|
+
SDK answer with the platform typia asserts (`INVALID_RESPONSE` otherwise), and
|
|
129
|
+
normalizes every SDK failure through `mapError` into a `CommerceError` whose
|
|
130
|
+
`storeCode` keeps the store's own code. It never grants and never sets
|
|
131
|
+
`settlement`; the backend ledger decides.
|
|
132
|
+
|
|
133
|
+
Two failure classes are not terminal. When `requestPurchase` throws or
|
|
134
|
+
returns a code that normalizes to `OUTCOME_UNKNOWN` (for example
|
|
135
|
+
`PLAY_BILLING_TIMEOUT`, `PLAY_BILLING_INTERRUPTED`,
|
|
136
|
+
`STOREKIT_PURCHASE_UNCERTAIN`) or `ALREADY_OWNED`, an unfinished purchase may
|
|
137
|
+
exist, so the provider answers `{ outcome: 'launched' }` instead of `failed`
|
|
138
|
+
and does not forward a `purchaseError` event carrying those codes
|
|
139
|
+
(`recoverableErrorCodes`). The attempt stays open and the reconciler resolves
|
|
140
|
+
it from `getAvailablePurchases`; the store UI is never reopened blindly. A
|
|
141
|
+
`capabilities.lifecycle` override may hide `restore` or `finish`, but it
|
|
142
|
+
cannot advertise one the SDK lacks: `restore: true` without
|
|
143
|
+
`sdk.restorePurchases`, like `finish: 'client' | 'host'` without
|
|
144
|
+
`sdk.finishTransaction`, is rejected when the provider is created.
|
|
145
|
+
|
|
146
|
+
`createCommerceServiceProvider` wraps the provider as a `CapacitorServiceProvider`
|
|
147
|
+
whose `commerce` field the registry recognizes: the ten commerce bridge
|
|
148
|
+
methods are implied (declaring a v1 `commerce.*` method alongside is an
|
|
149
|
+
error), `nativeIap` and `subscriptionIap` readiness derive from
|
|
150
|
+
`commerce.getCapabilities()`, and the in-process bridge delivers events as
|
|
151
|
+
`{ protocol: 'mpgd.commerce.v2', sequence, event }` with a per-session
|
|
152
|
+
sequence starting at 0. v1 callers keep working: `gateway.commerce` is served
|
|
153
|
+
by `toCommerceAdapter(provider)`, so a `launched` purchase is `pending`, a
|
|
154
|
+
synchronous purchase is `completed` with evidence and no entitlements, and
|
|
155
|
+
`restore` returns no entitlements. The gateway exposes the v2 surface as
|
|
156
|
+
`gateway.commerceV2` (served through the same validated bridge path, which
|
|
157
|
+
the conformance test exercises for every store vector) and adds a `commerce`
|
|
158
|
+
block to `getCapabilities()` from which `nativeIap` and `subscriptionIap` are
|
|
159
|
+
derived. The block comes from the registry's one bounded read per snapshot
|
|
160
|
+
(`getCapabilitySnapshot`, three-second timeout, shape validated): a stalled,
|
|
161
|
+
failing, or invalid provider yields a degraded block
|
|
162
|
+
(`createDegradedCommerceCapabilities`: `availability:
|
|
163
|
+
'temporarily-unavailable'`, nothing sold, `nativeIap: false`) and never
|
|
164
|
+
rejects the capability read. A `launched` v2 purchase keeps
|
|
165
|
+
`beginExternalActivity()` open after the request returns, since the store UI
|
|
166
|
+
is still up: the gateway releases it when the provider reports that product
|
|
167
|
+
(`purchaseUpdated` or `purchaseError`), after
|
|
168
|
+
`launchedPurchaseActivityTimeoutMs` (two minutes), or on `lifecycle.dispose`;
|
|
169
|
+
the v1 `commerce.purchase` path has no event to wait for and releases when
|
|
170
|
+
its request returns. Without a v2 provider nothing changes: `commerceV2` is
|
|
171
|
+
absent and a `commerce.v2.*` request still answers `COMMERCE_V2_UNAVAILABLE`
|
|
172
|
+
as the transport left it.
|
|
173
|
+
|
|
67
174
|
## App Store server-side transaction signatures
|
|
68
175
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
`@mpgd/
|
|
73
|
-
|
|
74
|
-
the
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
176
|
+
The server-side verifier moved to `@mpgd/game-services/app-store-server`
|
|
177
|
+
(see the [game-services README](../../packages/game-services/README.md)):
|
|
178
|
+
install the optional `@apple/app-store-server-library@3.1.0` peer next to
|
|
179
|
+
`@mpgd/game-services` and import `createAppleSignedTransactionVerifier` from
|
|
180
|
+
there. `@mpgd/adapter-capacitor/app-store-server` is deprecated: it re-exports
|
|
181
|
+
the same function and options type for one release and is removed in the next
|
|
182
|
+
major version. Supply DER-encoded Apple root certificates from the
|
|
183
|
+
[Apple PKI](https://www.apple.com/certificateauthority/), the game's bundle
|
|
184
|
+
ID, environment, and production App Apple ID. Use the result as
|
|
185
|
+
`signedTransactionVerifier` in `createAppStoreGameServicesEvidenceVerifier`.
|
|
186
|
+
Keep App Store API credentials, trust roots, and this verifier on the backend,
|
|
187
|
+
not in the Capacitor game; the adapter itself no longer peers on Apple's
|
|
188
|
+
library.
|
|
78
189
|
|
|
79
190
|
The server fetches the transaction from Apple's API, verifies its JWS, checks
|
|
80
191
|
the product, bundle, environment, account token and transaction ID, then
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Moved to `@mpgd/game-services/app-store-server` in A4 of the
|
|
3
|
+
* commerce v2 plan: server-side App Store verification does not belong in the
|
|
4
|
+
* client adapter. This subpath re-exports the same symbols for one release and
|
|
5
|
+
* will be removed in the next major version. Install
|
|
6
|
+
* `@apple/app-store-server-library` next to `@mpgd/game-services`.
|
|
7
|
+
*/
|
|
8
|
+
import { createAppleSignedTransactionVerifier as createAppleSignedTransactionVerifierFromGameServices, type CreateAppleSignedTransactionVerifierOptions as GameServicesAppleVerifierOptions } from '@mpgd/game-services/app-store-server';
|
|
9
|
+
/** @deprecated Import `createAppleSignedTransactionVerifier` from `@mpgd/game-services/app-store-server`. */
|
|
10
|
+
export declare const createAppleSignedTransactionVerifier: typeof createAppleSignedTransactionVerifierFromGameServices;
|
|
11
|
+
/** @deprecated Import `CreateAppleSignedTransactionVerifierOptions` from `@mpgd/game-services/app-store-server`. */
|
|
12
|
+
export type CreateAppleSignedTransactionVerifierOptions = GameServicesAppleVerifierOptions;
|
package/dist/app-store-server.js
CHANGED
|
@@ -1,119 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (options.environment !== 'Production' && options.environment !== 'Sandbox') {
|
|
12
|
-
throw new TypeError('App Store environment must be Production or Sandbox.');
|
|
13
|
-
}
|
|
14
|
-
if (options.bundleId.trim() === '') {
|
|
15
|
-
throw new TypeError('App Store bundle ID is required.');
|
|
16
|
-
}
|
|
17
|
-
if (options.environment === 'Production'
|
|
18
|
-
&& (!Number.isSafeInteger(options.appAppleId) || (options.appAppleId ?? 0) <= 0)) {
|
|
19
|
-
throw new TypeError('Production App Store verification requires appAppleId.');
|
|
20
|
-
}
|
|
21
|
-
if (options.environment === 'Production' && options.enableOnlineChecks === false) {
|
|
22
|
-
throw new TypeError('Production App Store verification requires online certificate checks.');
|
|
23
|
-
}
|
|
24
|
-
const verifier = new SignedDataVerifier(options.rootCertificates.map((certificate) => Buffer.from(certificate)), options.enableOnlineChecks ?? true, options.environment === 'Production' ? Environment.PRODUCTION : Environment.SANDBOX, options.bundleId, options.appAppleId);
|
|
25
|
-
return {
|
|
26
|
-
async verifyAndDecode(input) {
|
|
27
|
-
if (input.environment !== options.environment || input.bundleId !== options.bundleId) {
|
|
28
|
-
return { status: 'rejected', reason: 'APP_STORE_VERIFIER_SCOPE_MISMATCH' };
|
|
29
|
-
}
|
|
30
|
-
if (typeof input.signedTransaction !== 'string'
|
|
31
|
-
|| input.signedTransaction.length === 0
|
|
32
|
-
|| input.signedTransaction.length > 512 * 1024) {
|
|
33
|
-
return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_INVALID' };
|
|
34
|
-
}
|
|
35
|
-
input.signal.throwIfAborted();
|
|
36
|
-
let decoded;
|
|
37
|
-
try {
|
|
38
|
-
decoded = await waitForVerification(verifier.verifyAndDecodeTransaction(input.signedTransaction), input.signal);
|
|
39
|
-
input.signal.throwIfAborted();
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (input.signal.aborted) {
|
|
43
|
-
throw input.signal.reason;
|
|
44
|
-
}
|
|
45
|
-
if (error instanceof VerificationException) {
|
|
46
|
-
if (error.status === VerificationStatus.RETRYABLE_VERIFICATION_FAILURE) {
|
|
47
|
-
throw new AppStoreDependencyUnavailableError('Apple signature checks are temporarily unavailable.', error);
|
|
48
|
-
}
|
|
49
|
-
return { status: 'rejected', reason: 'APP_STORE_SIGNATURE_INVALID' };
|
|
50
|
-
}
|
|
51
|
-
// Only Apple's explicit retryable status is a transient dependency
|
|
52
|
-
// failure. Unexpected adapter/library faults must reach the caller.
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
const payload = decodeOneTimeTransaction(decoded);
|
|
56
|
-
return payload === undefined
|
|
57
|
-
? { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_INVALID' }
|
|
58
|
-
: { status: 'verified', payload };
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
async function waitForVerification(operation, signal) {
|
|
63
|
-
signal.throwIfAborted();
|
|
64
|
-
let onAbort;
|
|
65
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
66
|
-
onAbort = () => reject(signal.reason ?? new Error('Verification was aborted.'));
|
|
67
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
68
|
-
if (signal.aborted) {
|
|
69
|
-
onAbort();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
try {
|
|
73
|
-
return await Promise.race([operation, aborted]);
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
if (onAbort !== undefined) {
|
|
77
|
-
signal.removeEventListener('abort', onAbort);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function decodeOneTimeTransaction(decoded) {
|
|
82
|
-
if (!nonEmpty(decoded.transactionId)
|
|
83
|
-
|| !nonEmpty(decoded.originalTransactionId)
|
|
84
|
-
|| !nonEmpty(decoded.bundleId)
|
|
85
|
-
|| !nonEmpty(decoded.productId)
|
|
86
|
-
|| !validTimestamp(decoded.purchaseDate)
|
|
87
|
-
|| !validTimestamp(decoded.signedDate)
|
|
88
|
-
|| (decoded.environment !== 'Production' && decoded.environment !== 'Sandbox')
|
|
89
|
-
|| (decoded.type !== 'Consumable' && decoded.type !== 'Non-Consumable')
|
|
90
|
-
|| (decoded.quantity !== undefined
|
|
91
|
-
&& (!Number.isSafeInteger(decoded.quantity) || decoded.quantity <= 0))
|
|
92
|
-
|| (decoded.appAccountToken !== undefined && !nonEmpty(decoded.appAccountToken))
|
|
93
|
-
|| (decoded.revocationDate !== undefined && !validTimestamp(decoded.revocationDate))
|
|
94
|
-
|| (decoded.expiresDate !== undefined && !validTimestamp(decoded.expiresDate))
|
|
95
|
-
|| (decoded.isUpgraded !== undefined && typeof decoded.isUpgraded !== 'boolean')) {
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
transactionId: decoded.transactionId,
|
|
100
|
-
originalTransactionId: decoded.originalTransactionId,
|
|
101
|
-
bundleId: decoded.bundleId,
|
|
102
|
-
productId: decoded.productId,
|
|
103
|
-
purchaseDate: decoded.purchaseDate,
|
|
104
|
-
signedDate: decoded.signedDate,
|
|
105
|
-
environment: decoded.environment,
|
|
106
|
-
type: decoded.type,
|
|
107
|
-
...(decoded.quantity === undefined ? {} : { quantity: decoded.quantity }),
|
|
108
|
-
...(decoded.appAccountToken === undefined ? {} : { appAccountToken: decoded.appAccountToken }),
|
|
109
|
-
...(decoded.revocationDate === undefined ? {} : { revocationDate: decoded.revocationDate }),
|
|
110
|
-
...(decoded.expiresDate === undefined ? {} : { expiresDate: decoded.expiresDate }),
|
|
111
|
-
...(decoded.isUpgraded === undefined ? {} : { isUpgraded: decoded.isUpgraded }),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
function nonEmpty(value) {
|
|
115
|
-
return typeof value === 'string' && value.trim() !== '';
|
|
116
|
-
}
|
|
117
|
-
function validTimestamp(value) {
|
|
118
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
119
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Moved to `@mpgd/game-services/app-store-server` in A4 of the
|
|
3
|
+
* commerce v2 plan: server-side App Store verification does not belong in the
|
|
4
|
+
* client adapter. This subpath re-exports the same symbols for one release and
|
|
5
|
+
* will be removed in the next major version. Install
|
|
6
|
+
* `@apple/app-store-server-library` next to `@mpgd/game-services`.
|
|
7
|
+
*/
|
|
8
|
+
import { createAppleSignedTransactionVerifier as createAppleSignedTransactionVerifierFromGameServices, } from '@mpgd/game-services/app-store-server';
|
|
9
|
+
/** @deprecated Import `createAppleSignedTransactionVerifier` from `@mpgd/game-services/app-store-server`. */
|
|
10
|
+
export const createAppleSignedTransactionVerifier = createAppleSignedTransactionVerifierFromGameServices;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type BridgeCommerceV2Method, type BridgeEvent, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
2
|
+
import { type PlatformProviderFeature } from '@mpgd/platform';
|
|
3
|
+
import { type CommerceBridgeMethods, type CommerceProvider, type StoreId } from '@mpgd/platform/commerce';
|
|
4
|
+
import type { CapacitorCommerceServiceProvider } from './providers.js';
|
|
5
|
+
/** Bridge error for a commerce v2 payload the typia assert rejected. */
|
|
6
|
+
export declare const commerceV2InvalidPayloadBridgeErrorCode: 'COMMERCE_V2_INVALID_PAYLOAD';
|
|
7
|
+
/** Bridge error for a v2 method the provider does not implement (`finishTransaction`). */
|
|
8
|
+
export declare const commerceV2MethodUnsupportedBridgeErrorCode: 'COMMERCE_V2_METHOD_UNSUPPORTED';
|
|
9
|
+
export interface CommerceProviderBridge {
|
|
10
|
+
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
11
|
+
/** Delivers `{ protocol: 'mpgd.commerce.v2', sequence, event }`; `sequence` starts at 0 per bridge. */
|
|
12
|
+
subscribe(listener: (event: BridgeEvent) => void): () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface CreateCommerceProviderBridgeInput {
|
|
15
|
+
readonly provider: CommerceProvider;
|
|
16
|
+
/** Serves the methods the provider does not own, for example a plugin's ad methods. */
|
|
17
|
+
readonly fallback?: {
|
|
18
|
+
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* In-process `NativeBridge` over a commerce v2 provider: the six
|
|
23
|
+
* `commerce.v2.*` methods validate their payload and result with the
|
|
24
|
+
* `@mpgd/platform/commerce` asserts, the four v1 `commerce.*` methods are
|
|
25
|
+
* served by `toCommerceAdapter(provider)`, and events carry the bridge envelope
|
|
26
|
+
* with a per-session sequence. A thrown failure crosses the bridge as
|
|
27
|
+
* `{ code, message, retryable }`; a `cancelled` or `failed` purchase result
|
|
28
|
+
* keeps its `storeCode` because it is data, not an error.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createCommerceProviderBridge(input: CreateCommerceProviderBridgeInput): CommerceProviderBridge;
|
|
31
|
+
export type CapacitorCommerceFeature = Extract<PlatformProviderFeature, 'nativeIap' | 'subscriptionIap'>;
|
|
32
|
+
export interface CreateCommerceServiceProviderInput {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly commerce: CommerceProvider;
|
|
35
|
+
/** Commerce features the provider serves. Defaults to `['nativeIap']`. */
|
|
36
|
+
readonly features?: readonly CapacitorCommerceFeature[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Registry entry for a commerce v2 provider: the implied commerce bridge
|
|
40
|
+
* methods are served in process by `createCommerceProviderBridge`, and the
|
|
41
|
+
* registry derives `nativeIap` and `subscriptionIap` readiness from
|
|
42
|
+
* `commerce.getCapabilities()`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createCommerceServiceProvider(input: CreateCommerceServiceProviderInput): CapacitorCommerceServiceProvider;
|
|
45
|
+
/** Validates a commerce v2 result; a provider answer the contract rejects never reaches the caller. */
|
|
46
|
+
export declare function assertCommerceBridgeResult<TMethod extends BridgeCommerceV2Method>(method: TMethod, data: unknown): CommerceBridgeMethods[TMethod]['result'];
|
|
47
|
+
export interface CreateBridgeCommerceProviderInput {
|
|
48
|
+
readonly store: StoreId;
|
|
49
|
+
/** Sends one validated bridge request; the gateway's `request` fits. */
|
|
50
|
+
readonly request: <TData>(method: BridgeCommerceV2Method, payload: unknown) => Promise<TData>;
|
|
51
|
+
readonly subscribe: (listener: (event: BridgeEvent) => void) => () => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `CommerceProvider` over a bridge that serves the `commerce.v2.*` methods and
|
|
55
|
+
* the event envelope. Results are validated again on this side, so a transport
|
|
56
|
+
* that forgets to is still fail-closed; bridge errors become `CommerceError`s
|
|
57
|
+
* with the bridge code kept as `storeCode` when it is not already a v2 code.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createBridgeCommerceProvider(input: CreateBridgeCommerceProviderInput): CommerceProvider;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { assertBridgeEvent, bridgeEventProtocol, 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, CommerceError, commerceErrorInfoOf, commerceProtocol, commerceProtocolVersion, toCommerceAdapter, } from '@mpgd/platform/commerce';
|
|
4
|
+
import { normalizeStoreSdkError } from './store-sdk.js';
|
|
5
|
+
/** Bridge error for a commerce v2 payload the typia assert rejected. */
|
|
6
|
+
export const commerceV2InvalidPayloadBridgeErrorCode = 'COMMERCE_V2_INVALID_PAYLOAD';
|
|
7
|
+
/** Bridge error for a v2 method the provider does not implement (`finishTransaction`). */
|
|
8
|
+
export const commerceV2MethodUnsupportedBridgeErrorCode = 'COMMERCE_V2_METHOD_UNSUPPORTED';
|
|
9
|
+
const v1CommerceMethods = Object.freeze([
|
|
10
|
+
'commerce.getProducts',
|
|
11
|
+
'commerce.purchase',
|
|
12
|
+
'commerce.restore',
|
|
13
|
+
'commerce.getEntitlements',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* In-process `NativeBridge` over a commerce v2 provider: the six
|
|
17
|
+
* `commerce.v2.*` methods validate their payload and result with the
|
|
18
|
+
* `@mpgd/platform/commerce` asserts, the four v1 `commerce.*` methods are
|
|
19
|
+
* served by `toCommerceAdapter(provider)`, and events carry the bridge envelope
|
|
20
|
+
* with a per-session sequence. A thrown failure crosses the bridge as
|
|
21
|
+
* `{ code, message, retryable }`; a `cancelled` or `failed` purchase result
|
|
22
|
+
* keeps its `storeCode` because it is data, not an error.
|
|
23
|
+
*/
|
|
24
|
+
export function createCommerceProviderBridge(input) {
|
|
25
|
+
const { provider } = input;
|
|
26
|
+
const adapter = toCommerceAdapter(provider);
|
|
27
|
+
const listeners = new Set();
|
|
28
|
+
let sequence = 0;
|
|
29
|
+
let detach;
|
|
30
|
+
const deliver = (event) => {
|
|
31
|
+
const envelope = { protocol: commerceProtocol, sequence, event };
|
|
32
|
+
sequence += 1;
|
|
33
|
+
for (const listener of [...listeners]) {
|
|
34
|
+
listener(envelope);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const serve = async (request) => {
|
|
38
|
+
const { payload } = request;
|
|
39
|
+
switch (request.method) {
|
|
40
|
+
case 'commerce.v2.getCapabilities': {
|
|
41
|
+
assertCommerceBridgeGetCapabilitiesPayload(payload);
|
|
42
|
+
return assertResult('commerce.v2.getCapabilities', {
|
|
43
|
+
protocolVersion: commerceProtocolVersion,
|
|
44
|
+
store: provider.store,
|
|
45
|
+
capabilities: await provider.getCapabilities(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
case 'commerce.v2.fetchProducts': {
|
|
49
|
+
const products = await provider.fetchProducts(assertCommerceBridgeFetchProductsPayload(payload));
|
|
50
|
+
return assertResult('commerce.v2.fetchProducts', { products });
|
|
51
|
+
}
|
|
52
|
+
case 'commerce.v2.requestPurchase':
|
|
53
|
+
return assertResult('commerce.v2.requestPurchase', await provider.requestPurchase(assertCommerceBridgeRequestPurchasePayload(payload)));
|
|
54
|
+
case 'commerce.v2.getAvailablePurchases': {
|
|
55
|
+
const purchases = await provider.getAvailablePurchases(
|
|
56
|
+
// The payload is asserted as sent; a caller meaning the default sends `{}`.
|
|
57
|
+
assertCommerceBridgeGetAvailablePurchasesPayload(payload));
|
|
58
|
+
return assertResult('commerce.v2.getAvailablePurchases', { purchases });
|
|
59
|
+
}
|
|
60
|
+
case 'commerce.v2.finishTransaction': {
|
|
61
|
+
const finish = assertCommerceBridgeFinishTransactionPayload(payload);
|
|
62
|
+
if (provider.finishTransaction === undefined) {
|
|
63
|
+
throw new PlatformOperationError({
|
|
64
|
+
code: commerceV2MethodUnsupportedBridgeErrorCode,
|
|
65
|
+
message: `Store ${provider.store} does not finish transactions on the client.`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return assertResult('commerce.v2.finishTransaction', await provider.finishTransaction(finish));
|
|
69
|
+
}
|
|
70
|
+
case 'commerce.v2.restorePurchases': {
|
|
71
|
+
assertCommerceBridgeRestorePurchasesPayload(payload);
|
|
72
|
+
if (provider.restorePurchases === undefined) {
|
|
73
|
+
return assertResult('commerce.v2.restorePurchases', { restored: false });
|
|
74
|
+
}
|
|
75
|
+
await provider.restorePurchases();
|
|
76
|
+
return assertResult('commerce.v2.restorePurchases', { restored: true });
|
|
77
|
+
}
|
|
78
|
+
case 'commerce.getProducts':
|
|
79
|
+
return adapter.getProducts();
|
|
80
|
+
case 'commerce.purchase':
|
|
81
|
+
return adapter.purchase(payload);
|
|
82
|
+
case 'commerce.restore':
|
|
83
|
+
if (adapter.restore === undefined) {
|
|
84
|
+
return { restoredEntitlements: [] };
|
|
85
|
+
}
|
|
86
|
+
return adapter.restore();
|
|
87
|
+
case 'commerce.getEntitlements':
|
|
88
|
+
return adapter.getEntitlements();
|
|
89
|
+
default:
|
|
90
|
+
throw new PlatformOperationError({
|
|
91
|
+
code: 'UNSUPPORTED_METHOD',
|
|
92
|
+
message: `${request.method} is not a commerce method.`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
async request(request) {
|
|
98
|
+
const isCommerce = isBridgeCommerceV2Method(request.method)
|
|
99
|
+
|| v1CommerceMethods.includes(request.method);
|
|
100
|
+
if (!isCommerce && input.fallback !== undefined) {
|
|
101
|
+
return input.fallback.request(request);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return { id: request.id, ok: true, data: await serve(request) };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return toBridgeError(request, error);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
subscribe(listener) {
|
|
111
|
+
listeners.add(listener);
|
|
112
|
+
if (listeners.size === 1) {
|
|
113
|
+
detach = provider.subscribe(deliver);
|
|
114
|
+
}
|
|
115
|
+
return () => {
|
|
116
|
+
if (!listeners.delete(listener) || listeners.size > 0) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
detach?.();
|
|
120
|
+
detach = undefined;
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Registry entry for a commerce v2 provider: the implied commerce bridge
|
|
127
|
+
* methods are served in process by `createCommerceProviderBridge`, and the
|
|
128
|
+
* registry derives `nativeIap` and `subscriptionIap` readiness from
|
|
129
|
+
* `commerce.getCapabilities()`.
|
|
130
|
+
*/
|
|
131
|
+
export function createCommerceServiceProvider(input) {
|
|
132
|
+
return {
|
|
133
|
+
id: input.id,
|
|
134
|
+
features: input.features ?? ['nativeIap'],
|
|
135
|
+
methods: [],
|
|
136
|
+
commerce: input.commerce,
|
|
137
|
+
bridge: createCommerceProviderBridge({ provider: input.commerce }),
|
|
138
|
+
async getAvailability() {
|
|
139
|
+
return {};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Each entry is the typia assert of that method's result type; the lookup is
|
|
144
|
+
// typed by `assertCommerceBridgeResult`.
|
|
145
|
+
const resultAsserts = {
|
|
146
|
+
'commerce.v2.getCapabilities': assertCommerceBridgeGetCapabilitiesResult,
|
|
147
|
+
'commerce.v2.fetchProducts': assertCommerceBridgeFetchProductsResult,
|
|
148
|
+
'commerce.v2.requestPurchase': assertCommerceBridgeRequestPurchaseResult,
|
|
149
|
+
'commerce.v2.getAvailablePurchases': assertCommerceBridgeGetAvailablePurchasesResult,
|
|
150
|
+
'commerce.v2.finishTransaction': assertCommerceBridgeFinishTransactionResult,
|
|
151
|
+
'commerce.v2.restorePurchases': assertCommerceBridgeRestorePurchasesResult,
|
|
152
|
+
};
|
|
153
|
+
/** Validates a commerce v2 result; a provider answer the contract rejects never reaches the caller. */
|
|
154
|
+
export function assertCommerceBridgeResult(method, data) {
|
|
155
|
+
return resultAsserts[method](data);
|
|
156
|
+
}
|
|
157
|
+
function assertResult(method, data) {
|
|
158
|
+
try {
|
|
159
|
+
return assertCommerceBridgeResult(method, data);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw new PlatformOperationError({
|
|
163
|
+
code: 'NATIVE_PROVIDER_INVALID_RESPONSE',
|
|
164
|
+
message: `Commerce provider returned invalid data for ${method}: ${error instanceof Error ? error.message : String(error)}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function toBridgeError(request, error) {
|
|
169
|
+
if (isTypiaGuardError(error)) {
|
|
170
|
+
return createBridgeError(request.id, commerceV2InvalidPayloadBridgeErrorCode, `${request.method} payload is invalid: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
if (error instanceof PlatformOperationError) {
|
|
173
|
+
return createBridgeError(request.id, error.code, error.message, error.retryable);
|
|
174
|
+
}
|
|
175
|
+
const info = commerceErrorInfoOf(error);
|
|
176
|
+
return createBridgeError(request.id, info.code, info.message ?? `${request.method} failed.`, info.retryable);
|
|
177
|
+
}
|
|
178
|
+
function isTypiaGuardError(error) {
|
|
179
|
+
return error instanceof Error && error.name === 'TypeGuardError';
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* `CommerceProvider` over a bridge that serves the `commerce.v2.*` methods and
|
|
183
|
+
* the event envelope. Results are validated again on this side, so a transport
|
|
184
|
+
* that forgets to is still fail-closed; bridge errors become `CommerceError`s
|
|
185
|
+
* with the bridge code kept as `storeCode` when it is not already a v2 code.
|
|
186
|
+
*/
|
|
187
|
+
export function createBridgeCommerceProvider(input) {
|
|
188
|
+
const send = async (method, payload) => {
|
|
189
|
+
let data;
|
|
190
|
+
try {
|
|
191
|
+
data = await input.request(method, payload);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw toCommerceError(error);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return assertCommerceBridgeResult(method, data);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
throw new CommerceError('INVALID_RESPONSE', {
|
|
201
|
+
message: `Bridge returned invalid data for ${method}.`,
|
|
202
|
+
cause: error,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
store: input.store,
|
|
208
|
+
async getCapabilities() {
|
|
209
|
+
return (await send('commerce.v2.getCapabilities', { protocol: commerceProtocol }))
|
|
210
|
+
.capabilities;
|
|
211
|
+
},
|
|
212
|
+
async fetchProducts(request) {
|
|
213
|
+
return (await send('commerce.v2.fetchProducts', request)).products;
|
|
214
|
+
},
|
|
215
|
+
requestPurchase(request) {
|
|
216
|
+
return send('commerce.v2.requestPurchase', request);
|
|
217
|
+
},
|
|
218
|
+
async getAvailablePurchases(request = {}) {
|
|
219
|
+
return (await send('commerce.v2.getAvailablePurchases', request)).purchases;
|
|
220
|
+
},
|
|
221
|
+
finishTransaction(request) {
|
|
222
|
+
return send('commerce.v2.finishTransaction', request);
|
|
223
|
+
},
|
|
224
|
+
async restorePurchases() {
|
|
225
|
+
await send('commerce.v2.restorePurchases', { protocol: commerceProtocol });
|
|
226
|
+
},
|
|
227
|
+
subscribe(listener) {
|
|
228
|
+
return input.subscribe((envelope) => {
|
|
229
|
+
// Another protocol on a shared event channel is not ours to judge.
|
|
230
|
+
if (typeof envelope === 'object' && envelope !== null
|
|
231
|
+
&& envelope.protocol !== bridgeEventProtocol) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let event;
|
|
235
|
+
try {
|
|
236
|
+
event = assertBridgeEvent(envelope);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
listener({
|
|
240
|
+
type: 'purchaseError',
|
|
241
|
+
error: new CommerceError('INVALID_RESPONSE', {
|
|
242
|
+
message: 'Bridge delivered an invalid commerce event.',
|
|
243
|
+
cause: error,
|
|
244
|
+
}).toCommerceErrorInfo(),
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
listener(event.event);
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function toCommerceError(error) {
|
|
254
|
+
if (error instanceof CommerceError) {
|
|
255
|
+
return error;
|
|
256
|
+
}
|
|
257
|
+
if (error instanceof PlatformOperationError) {
|
|
258
|
+
return normalizeStoreSdkError({
|
|
259
|
+
code: error.code,
|
|
260
|
+
message: error.message,
|
|
261
|
+
retryable: error.retryable,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return normalizeStoreSdkError(error);
|
|
265
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { type PlatformGateway, type PlatformTarget, type ViewportAdapter } from '@mpgd/platform';
|
|
2
2
|
import { type CapacitorAppEventsApi, type CapacitorIncomingUrlKind, type CapacitorVisibilitySource } from './app-events.js';
|
|
3
3
|
import { type CapacitorServiceProvider, type NativeBridge } from './providers.js';
|
|
4
|
-
export { createCapacitorProviderRegistry, type CapacitorServiceProvider, type NativeBridge, } from './providers.js';
|
|
4
|
+
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, deriveCommerceFeatureStates, type CapacitorCommerceServiceProvider, type CapacitorProviderCapabilitySnapshot, type CapacitorProviderRegistry, type CapacitorServiceProvider, type NativeBridge, } from './providers.js';
|
|
5
|
+
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, type CapacitorCommerceFeature, type CommerceProviderBridge, type CreateBridgeCommerceProviderInput, type CreateCommerceProviderBridgeInput, type CreateCommerceServiceProviderInput, } from './commerce-bridge.js';
|
|
6
|
+
export { createStoreCommerceProvider, recoverableErrorCodes, type CreateStoreCommerceProviderInput, type StoreCommerceCapabilityOverrides, type StoreCommerceProductMapping, type StoreCommerceProvider, } from './store-commerce-provider.js';
|
|
7
|
+
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';
|
|
5
8
|
export { createCapacitorAppEvents, type CapacitorAppEventsApi, type CapacitorIncomingUrlKind, type CapacitorVisibilitySource, } from './app-events.js';
|
|
6
9
|
export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, type CapacitorJsonRequest, type CapacitorJsonResponse, type CapacitorNativeHttpErrorCode, type CapacitorNativeJsonTransport, type CreateCapacitorNativeJsonTransportInput, } from './native-http.js';
|
|
7
10
|
export { createCapacitorViewport, type CapacitorViewportBaseState, type CapacitorOccupiedSurfaceInput, type CapacitorViewportController, type CapacitorViewportHost, } from './viewport.js';
|
|
11
|
+
/**
|
|
12
|
+
* Upper bound on how long a `launched` commerce v2 purchase keeps the game
|
|
13
|
+
* paused when no purchase event for the product arrives (for example a store
|
|
14
|
+
* sheet dismissed without a callback).
|
|
15
|
+
*/
|
|
16
|
+
export declare const launchedPurchaseActivityTimeoutMs = 120000;
|
|
8
17
|
export declare function createCapacitorPlatformGateway(input: {
|
|
9
18
|
readonly target: Extract<PlatformTarget, 'android' | 'ios'>;
|
|
10
19
|
readonly appVersion: string;
|