@mpgd/adapter-capacitor 0.9.2 → 0.10.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 +106 -46
- package/dist/app-events.d.ts +5 -1
- package/dist/app-events.js +43 -2
- package/dist/commerce-bridge.d.ts +10 -14
- package/dist/commerce-bridge.js +20 -36
- package/dist/index.d.ts +2 -2
- package/dist/index.js +47 -50
- package/dist/providers.d.ts +24 -23
- package/dist/providers.js +60 -172
- package/dist/request-policy.d.ts +16 -0
- package/dist/request-policy.js +36 -0
- package/package.json +7 -11
- package/dist/app-store-server.d.ts +0 -12
- package/dist/app-store-server.js +0 -10
package/README.md
CHANGED
|
@@ -31,14 +31,17 @@ export function createGameGateway(providers: readonly CapacitorServiceProvider[]
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
`getCapabilities()` reports a fresh boolean snapshot
|
|
35
|
-
`providerAvailability` detail
|
|
36
|
-
`configuration-required`, `action-required`,
|
|
37
|
-
`available`. A target's `features` configuration
|
|
38
|
-
proof that a provider is installed or ready.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
`getCapabilities()` reports a fresh boolean snapshot, optional
|
|
35
|
+
`providerAvailability` detail, and the `commerce` capability block. The
|
|
36
|
+
possible states are `unsupported`, `configuration-required`, `action-required`,
|
|
37
|
+
`temporarily-unavailable`, and `available`. A target's `features` configuration
|
|
38
|
+
is an upper bound, **not** proof that a provider is installed or ready.
|
|
39
|
+
Commerce is not a provider feature: the `commerce` block (see below) is the
|
|
40
|
+
only commerce source, and the retired `nativeIap` and `subscriptionIap` flags
|
|
41
|
+
are never reported, even when a native shell built before commerce v2 still
|
|
42
|
+
sends them. Rewarded, interstitial, and banner ads and native versus remote
|
|
43
|
+
leaderboards remain distinct. Target-config applies the configured upper
|
|
44
|
+
bound to this live provider state.
|
|
42
45
|
|
|
43
46
|
When native and remote leaderboard routes coexist, the target-config wrapper
|
|
44
47
|
selects a `route` on score/open calls; direct adapter callers can request
|
|
@@ -64,14 +67,14 @@ The source tests and installed-tarball consumer validate composition, types,
|
|
|
64
67
|
error paths, and fallback behavior. They do not establish that any optional
|
|
65
68
|
SDK works on a physical device or that a store/ad setup is release-ready.
|
|
66
69
|
|
|
67
|
-
## Commerce
|
|
70
|
+
## Commerce providers
|
|
68
71
|
|
|
69
|
-
Commerce
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
Commerce ([RFC](../../docs/specs/commerce/commerce-v2-rfc.md)) has one
|
|
73
|
+
surface: the `CommerceProvider` from `@mpgd/platform/commerce`, exposed as
|
|
74
|
+
`gateway.commerce` and served over the six `commerce.v2.*` bridge methods. A
|
|
75
|
+
store plugin exposes an in-process `StoreSdk`, the adapter turns it into a
|
|
76
|
+
`CommerceProvider`, and the provider registry serves it. The four v1
|
|
77
|
+
`commerce.*` bridge methods and the v1 gateway object are removed.
|
|
75
78
|
|
|
76
79
|
`StoreSdk` is shaped like the OpenIAP Client Protocol so a future Capacitor
|
|
77
80
|
IAP plugin over the OpenIAP cores is a drop-in: `fetchProducts({ skus, type })`,
|
|
@@ -143,43 +146,72 @@ cannot advertise one the SDK lacks: `restore: true` without
|
|
|
143
146
|
`sdk.restorePurchases`, like `finish: 'client' | 'host'` without
|
|
144
147
|
`sdk.finishTransaction`, is rejected when the provider is created.
|
|
145
148
|
|
|
146
|
-
`createCommerceServiceProvider` wraps the provider as a
|
|
147
|
-
whose `commerce` field the registry recognizes
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`
|
|
151
|
-
`{ protocol: 'mpgd.commerce.v2', sequence, event }` with a
|
|
152
|
-
sequence starting at 0.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`gateway.
|
|
157
|
-
the conformance test exercises for every store
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
`
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
149
|
+
`createCommerceServiceProvider({ id, commerce })` wraps the provider as a
|
|
150
|
+
`CapacitorServiceProvider` whose `commerce` field the registry recognizes. It
|
|
151
|
+
declares no `features` and no `methods`: the six `commerce.v2.*` bridge methods
|
|
152
|
+
are implied (declaring one, or a retired v1 `commerce.*` method, is an error),
|
|
153
|
+
the bridge must implement `subscribe`, and the in-process bridge delivers
|
|
154
|
+
events as `{ protocol: 'mpgd.commerce.v2', sequence, event }` with a
|
|
155
|
+
per-session sequence starting at 0. `CapacitorServiceProvider.features` lists
|
|
156
|
+
non-commerce features only (`PlatformProviderFeature` has no IAP names), and a
|
|
157
|
+
second commerce provider is a registration error.
|
|
158
|
+
|
|
159
|
+
The gateway exposes the provider as `gateway.commerce` (served through the same
|
|
160
|
+
validated bridge path, which the conformance test exercises for every store
|
|
161
|
+
vector) and omits the property when no provider is registered. The
|
|
162
|
+
`commerce` block of `getCapabilities()` is always present and is a fresh copy
|
|
163
|
+
on every read: the provider's validated capability block, or
|
|
164
|
+
`createUnsupportedCommerceCapabilities()` when no commerce provider is
|
|
165
|
+
registered. A native `runtime.getCapabilities` answer is only the base: its
|
|
166
|
+
`nativeIap` and `subscriptionIap` keys are stripped and its `commerce` block,
|
|
167
|
+
whatever it says, is replaced. The block comes from the registry's one bounded
|
|
168
|
+
read per snapshot (`getCapabilitySnapshot` returns `{ availability, commerce }`,
|
|
169
|
+
three-second timeout, shape validated): a stalled, failing, or invalid
|
|
170
|
+
provider yields a degraded block (`createDegradedCommerceCapabilities`:
|
|
171
|
+
`availability: 'temporarily-unavailable'`, nothing sold) and never rejects the
|
|
172
|
+
capability read. A `launched` purchase keeps `beginExternalActivity()` open
|
|
173
|
+
after the request returns, since the store UI is still up: the gateway releases
|
|
174
|
+
it when the provider reports that product (`purchaseUpdated` or
|
|
175
|
+
`purchaseError`), after `launchedPurchaseActivityTimeoutMs` (two minutes), or
|
|
176
|
+
on `lifecycle.dispose`. The registry does not gate a commerce method by a
|
|
177
|
+
snapshot: the provider reads its own `availability` and fails closed before any
|
|
178
|
+
store UI opens. A `commerce.v2.*` request that reaches a transport without a
|
|
179
|
+
provider still answers `COMMERCE_V2_UNAVAILABLE`.
|
|
180
|
+
|
|
181
|
+
Games do not call the provider from a scene: `createGameServicesClient` from
|
|
182
|
+
`@mpgd/game-services` purchases through `createCommerceReconciler` over
|
|
183
|
+
`gateway.commerce`, and nothing is granted from a client callback.
|
|
184
|
+
|
|
185
|
+
## Notifications v2
|
|
186
|
+
|
|
187
|
+
The notifications v2 provider arrives in PR P2 of the
|
|
188
|
+
[notifications RFC](../../docs/specs/notifications/notifications-v2-rfc.md),
|
|
189
|
+
together with the registry surface through which a provider serves the
|
|
190
|
+
`notifications.v2.*` methods. Until then the gateway exposes no
|
|
191
|
+
`notificationsV2`, and its request path never forwards a `notifications.v2.*`
|
|
192
|
+
method to the base plugin: it answers `NOTIFICATIONS_V2_UNAVAILABLE`, which is
|
|
193
|
+
not retryable, before it pauses the game. A provider cannot declare those
|
|
194
|
+
methods in `methods`.
|
|
195
|
+
|
|
196
|
+
Once P2 lets a provider serve `notifications.v2.*` methods, these rules apply:
|
|
197
|
+
|
|
198
|
+
- `notifications.v2.requestPermission` and `notifications.v2.openSettings`
|
|
199
|
+
pause the game while they run, like `notifications.requestSubscription` and
|
|
200
|
+
`commerce.v2.requestPurchase`.
|
|
201
|
+
- The provider registry passes `notifications.v2.*` through `assertMethodReady`
|
|
202
|
+
without a readiness check, as it does `commerce.v2.*`, so the provider's own
|
|
203
|
+
`unsupported` or `action-required` answers reach the game as answers.
|
|
204
|
+
- The gateway validates each answer before the game sees it. Until P2 adds the
|
|
205
|
+
notifications v2 answers to that check, it rejects every one of them.
|
|
173
206
|
|
|
174
207
|
## App Store server-side transaction signatures
|
|
175
208
|
|
|
176
|
-
The server-side verifier
|
|
209
|
+
The server-side verifier lives in `@mpgd/game-services/app-store-server`
|
|
177
210
|
(see the [game-services README](../../packages/game-services/README.md)):
|
|
178
211
|
install the optional `@apple/app-store-server-library@3.1.0` peer next to
|
|
179
212
|
`@mpgd/game-services` and import `createAppleSignedTransactionVerifier` from
|
|
180
|
-
there. `@mpgd/adapter-capacitor/app-store-server`
|
|
181
|
-
|
|
182
|
-
major version. Supply DER-encoded Apple root certificates from the
|
|
213
|
+
there. The former `@mpgd/adapter-capacitor/app-store-server` subpath, a
|
|
214
|
+
deprecated re-export, was removed. Supply DER-encoded Apple root certificates from the
|
|
183
215
|
[Apple PKI](https://www.apple.com/certificateauthority/), the game's bundle
|
|
184
216
|
ID, environment, and production App Apple ID. Use the result as
|
|
185
217
|
`signedTransactionVerifier` in `createAppStoreGameServicesEvidenceVerifier`.
|
|
@@ -194,6 +226,21 @@ evidence; it never grants a product. Retryable certificate-status failures
|
|
|
194
226
|
remain pending, while invalid signatures are rejected. Mock verification does
|
|
195
227
|
not prove a real App Store purchase or device delivery.
|
|
196
228
|
|
|
229
|
+
### Migrating from v1
|
|
230
|
+
|
|
231
|
+
Commerce v2 PR C4 removed the v1 surface in one major release.
|
|
232
|
+
|
|
233
|
+
| Removed | Replacement |
|
|
234
|
+
| --- | --- |
|
|
235
|
+
| `gateway.commerce.getProducts / purchase / restore / getEntitlements` (v1 `CommerceAdapter`) | `gateway.commerce` is now the `CommerceProvider` (`fetchProducts`, `requestPurchase`, `getAvailablePurchases`, `finishTransaction?`, `restorePurchases?`, `subscribe`); games purchase through `@mpgd/game-services` |
|
|
236
|
+
| `gateway.commerceV2` | `gateway.commerce` |
|
|
237
|
+
| `commerce.getProducts`, `commerce.purchase`, `commerce.restore`, `commerce.getEntitlements` bridge methods and `toCommerceAdapter` | the six `commerce.v2.*` methods |
|
|
238
|
+
| `nativeIap`, `subscriptionIap` in `getCapabilities()` and `providerAvailability` | `capabilities.commerce` (`purchase.consumable`, `purchase.durable`, `purchase.subscription`, `availability`) |
|
|
239
|
+
| `'nativeIap'` / `'subscriptionIap'` in `CapacitorServiceProvider.features`; the `features` input of `createCommerceServiceProvider`; `CapacitorCommerceFeature`; `deriveCommerceFeatureStates` | omit `features`: a provider with `commerce` declares none, and its readiness is its capability block |
|
|
240
|
+
| the `productType` parameter of `assertMethodReady`; `NATIVE_PROVIDER_PRODUCT_UNKNOWN`, `NATIVE_PROVIDER_PRODUCT_TYPE_REQUIRED` | none: product routing belongs to the commerce provider |
|
|
241
|
+
| `CapacitorProviderCapabilitySnapshot.commerce?` (optional) | always present: the provider block, the degraded block, or the unsupported block |
|
|
242
|
+
| `@mpgd/adapter-capacitor/app-store-server` | `@mpgd/game-services/app-store-server` |
|
|
243
|
+
|
|
197
244
|
## Opt-in AdMob rewarded ads
|
|
198
245
|
|
|
199
246
|
Install `@capacitor-community/admob@8.1.0` in the game-owned Capacitor project,
|
|
@@ -316,6 +363,19 @@ app. Cold URLs are read through `getInitialGameUrl` or
|
|
|
316
363
|
callback. Call `lifecycle.dispose()` when the host tears down this gateway;
|
|
317
364
|
the adapter removes only its own listener handles, never every App listener.
|
|
318
365
|
|
|
366
|
+
A game link is untrusted input. The adapter checks the link's path, query, and
|
|
367
|
+
fragment with `normalizeRootRelativeRoute` from `@mpgd/platform/routes` and
|
|
368
|
+
discards a link that rule refuses, such as `mygame://game//evil.example/x` or
|
|
369
|
+
`https://game.example/%2F%2Fevil.example/x`, because a client could read its
|
|
370
|
+
path as another host. A delivered event carries the checked path as `route`:
|
|
371
|
+
`/daily` for `mygame://game/daily`, as in the example above, where the host
|
|
372
|
+
only names the link kind. The host is never part of `route`, so
|
|
373
|
+
`mygame://level/3` gives `/3`. A router that routes by path opens
|
|
374
|
+
`event.route` rather than parsing `event.url`. A link with no path to check,
|
|
375
|
+
such as `mygame://daily`, arrives without `route`. A router that builds its
|
|
376
|
+
route from anything else in the URL, such as the host, checks that route with
|
|
377
|
+
`normalizeRootRelativeRoute` and opens only what it returns.
|
|
378
|
+
|
|
319
379
|
Save progress at checkpoints and transaction boundaries. A pause callback can
|
|
320
380
|
request a best-effort save, but neither pause nor back is guaranteed to run
|
|
321
381
|
before the operating system terminates a process. The lifecycle tests use an
|
package/dist/app-events.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ export interface CreateCapacitorAppEventsInput {
|
|
|
11
11
|
readonly target: 'android' | 'ios';
|
|
12
12
|
readonly app?: CapacitorAppEventsApi;
|
|
13
13
|
readonly visibility?: CapacitorVisibilitySource | null;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Return null for URLs that this game does not own. Never default OAuth to
|
|
16
|
+
* game. A game link is then delivered only when its path passes the shared
|
|
17
|
+
* route rule (see `NativeOpenUrlEvent.route`).
|
|
18
|
+
*/
|
|
15
19
|
readonly classifyIncomingUrl?: (url: string) => CapacitorIncomingUrlKind | null;
|
|
16
20
|
readonly historyBack?: () => void;
|
|
17
21
|
readonly onError?: (error: unknown) => void;
|
package/dist/app-events.js
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { App } from '@capacitor/app';
|
|
2
|
+
import { hasRouteControlCharacter, normalizeRootRelativeRoute } from '@mpgd/platform/routes';
|
|
2
3
|
const maxWarmUrlsBeforeInitial = 32;
|
|
3
4
|
// Covers a near-simultaneous duplicate from getLaunchUrl and appUrlOpen, not a later tap.
|
|
4
5
|
const coldUrlDedupeWindowMs = 2_000;
|
|
5
6
|
function defaultVisibility() {
|
|
6
7
|
return typeof document === 'undefined' ? null : document;
|
|
7
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads a game link the way `NativeOpenUrlEvent.route` describes: its path,
|
|
11
|
+
* query, and fragment as a normalized route (the host is not part of it),
|
|
12
|
+
* `undefined` when the URL has no path to check (`mygame:daily`,
|
|
13
|
+
* `mygame://daily`), or `null` when the link is not delivered: it does not
|
|
14
|
+
* parse, carries a control character, or has a path `normalizeRootRelativeRoute`
|
|
15
|
+
* refuses because a client could read it as another host.
|
|
16
|
+
*/
|
|
17
|
+
function readGameLinkRoute(url) {
|
|
18
|
+
if (hasRouteControlCharacter(url)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(url);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const pathname = parsed.pathname === '' && (parsed.search !== '' || parsed.hash !== '')
|
|
29
|
+
? '/'
|
|
30
|
+
: parsed.pathname;
|
|
31
|
+
if (!pathname.startsWith('/')) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return normalizeRootRelativeRoute(`${pathname}${parsed.search}${parsed.hash}`) ?? null;
|
|
35
|
+
}
|
|
36
|
+
function toOpenUrlEvent(url, source, route) {
|
|
37
|
+
return route === undefined ? { url, source } : { url, source, route };
|
|
38
|
+
}
|
|
8
39
|
/**
|
|
9
40
|
* Owns only handles it registers. Disposing during an awaited addListener
|
|
10
41
|
* removes the eventual handle without touching another gateway's listeners.
|
|
@@ -98,6 +129,12 @@ export function createCapacitorAppEvents(input) {
|
|
|
98
129
|
if (kind === null) {
|
|
99
130
|
return;
|
|
100
131
|
}
|
|
132
|
+
const route = kind === 'game' ? readGameLinkRoute(url) : undefined;
|
|
133
|
+
if (route === null) {
|
|
134
|
+
// A game link whose path could name another host is discarded like an
|
|
135
|
+
// unknown URL: no router may open it.
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
101
138
|
const callbacks = kind === 'game' ? gameUrlCallbacks : oauthCallbacks;
|
|
102
139
|
if (!initialResolved && callbacks.size > 0) {
|
|
103
140
|
// Keep enough recent entries to suppress a cold/warm duplicate without
|
|
@@ -110,7 +147,7 @@ export function createCapacitorAppEvents(input) {
|
|
|
110
147
|
}
|
|
111
148
|
warmUrlsBeforeInitial.add(url);
|
|
112
149
|
}
|
|
113
|
-
const event =
|
|
150
|
+
const event = toOpenUrlEvent(url, 'warm', route);
|
|
114
151
|
for (const callback of [...callbacks]) {
|
|
115
152
|
try {
|
|
116
153
|
callback(event);
|
|
@@ -239,9 +276,13 @@ export function createCapacitorAppEvents(input) {
|
|
|
239
276
|
if (kind === null) {
|
|
240
277
|
return null;
|
|
241
278
|
}
|
|
279
|
+
const route = kind === 'game' ? readGameLinkRoute(url) : undefined;
|
|
280
|
+
if (route === null) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
242
283
|
recentColdUrl = url;
|
|
243
284
|
recentColdUntil = Date.now() + coldUrlDedupeWindowMs;
|
|
244
|
-
return { kind, event:
|
|
285
|
+
return { kind, event: toOpenUrlEvent(url, 'cold', route) };
|
|
245
286
|
}, (error) => {
|
|
246
287
|
initialResolved = true;
|
|
247
288
|
warmUrlsBeforeInitial.clear();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type PlatformProviderFeature } from '@mpgd/platform';
|
|
1
|
+
import { type BridgeCommerceEvent, type BridgeCommerceV2Method, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
3
2
|
import { type CommerceBridgeMethods, type CommerceProvider, type StoreId } from '@mpgd/platform/commerce';
|
|
4
3
|
import type { CapacitorCommerceServiceProvider } from './providers.js';
|
|
5
4
|
/** Bridge error for a commerce v2 payload the typia assert rejected. */
|
|
@@ -9,7 +8,7 @@ export declare const commerceV2MethodUnsupportedBridgeErrorCode: 'COMMERCE_V2_ME
|
|
|
9
8
|
export interface CommerceProviderBridge {
|
|
10
9
|
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
11
10
|
/** Delivers `{ protocol: 'mpgd.commerce.v2', sequence, event }`; `sequence` starts at 0 per bridge. */
|
|
12
|
-
subscribe(listener: (event:
|
|
11
|
+
subscribe(listener: (event: BridgeCommerceEvent) => void): () => void;
|
|
13
12
|
}
|
|
14
13
|
export interface CreateCommerceProviderBridgeInput {
|
|
15
14
|
readonly provider: CommerceProvider;
|
|
@@ -21,25 +20,22 @@ export interface CreateCommerceProviderBridgeInput {
|
|
|
21
20
|
/**
|
|
22
21
|
* In-process `NativeBridge` over a commerce v2 provider: the six
|
|
23
22
|
* `commerce.v2.*` methods validate their payload and result with the
|
|
24
|
-
* `@mpgd/platform/commerce` asserts,
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* `@mpgd/platform/commerce` asserts, and events carry the bridge envelope
|
|
24
|
+
* with a per-session sequence. Any other method goes to `fallback`, or answers
|
|
25
|
+
* `UNSUPPORTED_METHOD` without one. A thrown failure crosses the bridge as
|
|
27
26
|
* `{ code, message, retryable }`; a `cancelled` or `failed` purchase result
|
|
28
27
|
* keeps its `storeCode` because it is data, not an error.
|
|
29
28
|
*/
|
|
30
29
|
export declare function createCommerceProviderBridge(input: CreateCommerceProviderBridgeInput): CommerceProviderBridge;
|
|
31
|
-
export type CapacitorCommerceFeature = Extract<PlatformProviderFeature, 'nativeIap' | 'subscriptionIap'>;
|
|
32
30
|
export interface CreateCommerceServiceProviderInput {
|
|
33
31
|
readonly id: string;
|
|
34
32
|
readonly commerce: CommerceProvider;
|
|
35
|
-
/** Commerce features the provider serves. Defaults to `['nativeIap']`. */
|
|
36
|
-
readonly features?: readonly CapacitorCommerceFeature[];
|
|
37
33
|
}
|
|
38
34
|
/**
|
|
39
|
-
* Registry entry for a commerce
|
|
40
|
-
* methods are served in process by
|
|
41
|
-
*
|
|
42
|
-
* `
|
|
35
|
+
* Registry entry for a commerce provider: it declares no feature and no
|
|
36
|
+
* method, the six `commerce.v2.*` methods are implied and served in process by
|
|
37
|
+
* `createCommerceProviderBridge`, and the gateway's `commerce` capability block
|
|
38
|
+
* is the provider's own `getCapabilities()` answer.
|
|
43
39
|
*/
|
|
44
40
|
export declare function createCommerceServiceProvider(input: CreateCommerceServiceProviderInput): CapacitorCommerceServiceProvider;
|
|
45
41
|
/** Validates a commerce v2 result; a provider answer the contract rejects never reaches the caller. */
|
|
@@ -48,7 +44,7 @@ export interface CreateBridgeCommerceProviderInput {
|
|
|
48
44
|
readonly store: StoreId;
|
|
49
45
|
/** Sends one validated bridge request; the gateway's `request` fits. */
|
|
50
46
|
readonly request: <TData>(method: BridgeCommerceV2Method, payload: unknown) => Promise<TData>;
|
|
51
|
-
readonly subscribe: (listener: (event:
|
|
47
|
+
readonly subscribe: (listener: (event: BridgeCommerceEvent) => void) => () => void;
|
|
52
48
|
}
|
|
53
49
|
/**
|
|
54
50
|
* `CommerceProvider` over a bridge that serves the `commerce.v2.*` methods and
|
package/dist/commerce-bridge.js
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { PlatformOperationError
|
|
3
|
-
import { assertCommerceBridgeFetchProductsPayload, assertCommerceBridgeFetchProductsResult, assertCommerceBridgeFinishTransactionPayload, assertCommerceBridgeFinishTransactionResult, assertCommerceBridgeGetAvailablePurchasesPayload, assertCommerceBridgeGetAvailablePurchasesResult, assertCommerceBridgeGetCapabilitiesPayload, assertCommerceBridgeGetCapabilitiesResult, assertCommerceBridgeRequestPurchasePayload, assertCommerceBridgeRequestPurchaseResult, assertCommerceBridgeRestorePurchasesPayload, assertCommerceBridgeRestorePurchasesResult, CommerceError, commerceErrorInfoOf, commerceProtocol, commerceProtocolVersion,
|
|
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';
|
|
4
4
|
import { normalizeStoreSdkError } from './store-sdk.js';
|
|
5
5
|
/** Bridge error for a commerce v2 payload the typia assert rejected. */
|
|
6
6
|
export const commerceV2InvalidPayloadBridgeErrorCode = 'COMMERCE_V2_INVALID_PAYLOAD';
|
|
7
7
|
/** Bridge error for a v2 method the provider does not implement (`finishTransaction`). */
|
|
8
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
9
|
/**
|
|
16
10
|
* In-process `NativeBridge` over a commerce v2 provider: the six
|
|
17
11
|
* `commerce.v2.*` methods validate their payload and result with the
|
|
18
|
-
* `@mpgd/platform/commerce` asserts,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
12
|
+
* `@mpgd/platform/commerce` asserts, and events carry the bridge envelope
|
|
13
|
+
* with a per-session sequence. Any other method goes to `fallback`, or answers
|
|
14
|
+
* `UNSUPPORTED_METHOD` without one. A thrown failure crosses the bridge as
|
|
21
15
|
* `{ code, message, retryable }`; a `cancelled` or `failed` purchase result
|
|
22
16
|
* keeps its `storeCode` because it is data, not an error.
|
|
23
17
|
*/
|
|
24
18
|
export function createCommerceProviderBridge(input) {
|
|
25
19
|
const { provider } = input;
|
|
26
|
-
const adapter = toCommerceAdapter(provider);
|
|
27
20
|
const listeners = new Set();
|
|
28
21
|
let sequence = 0;
|
|
29
22
|
let detach;
|
|
@@ -75,17 +68,6 @@ export function createCommerceProviderBridge(input) {
|
|
|
75
68
|
await provider.restorePurchases();
|
|
76
69
|
return assertResult('commerce.v2.restorePurchases', { restored: true });
|
|
77
70
|
}
|
|
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
71
|
default:
|
|
90
72
|
throw new PlatformOperationError({
|
|
91
73
|
code: 'UNSUPPORTED_METHOD',
|
|
@@ -95,9 +77,7 @@ export function createCommerceProviderBridge(input) {
|
|
|
95
77
|
};
|
|
96
78
|
return {
|
|
97
79
|
async request(request) {
|
|
98
|
-
|
|
99
|
-
|| v1CommerceMethods.includes(request.method);
|
|
100
|
-
if (!isCommerce && input.fallback !== undefined) {
|
|
80
|
+
if (!isBridgeCommerceV2Method(request.method) && input.fallback !== undefined) {
|
|
101
81
|
return input.fallback.request(request);
|
|
102
82
|
}
|
|
103
83
|
try {
|
|
@@ -123,15 +103,15 @@ export function createCommerceProviderBridge(input) {
|
|
|
123
103
|
};
|
|
124
104
|
}
|
|
125
105
|
/**
|
|
126
|
-
* Registry entry for a commerce
|
|
127
|
-
* methods are served in process by
|
|
128
|
-
*
|
|
129
|
-
* `
|
|
106
|
+
* Registry entry for a commerce provider: it declares no feature and no
|
|
107
|
+
* method, the six `commerce.v2.*` methods are implied and served in process by
|
|
108
|
+
* `createCommerceProviderBridge`, and the gateway's `commerce` capability block
|
|
109
|
+
* is the provider's own `getCapabilities()` answer.
|
|
130
110
|
*/
|
|
131
111
|
export function createCommerceServiceProvider(input) {
|
|
132
112
|
return {
|
|
133
113
|
id: input.id,
|
|
134
|
-
features:
|
|
114
|
+
features: [],
|
|
135
115
|
methods: [],
|
|
136
116
|
commerce: input.commerce,
|
|
137
117
|
bridge: createCommerceProviderBridge({ provider: input.commerce }),
|
|
@@ -226,14 +206,18 @@ export function createBridgeCommerceProvider(input) {
|
|
|
226
206
|
},
|
|
227
207
|
subscribe(listener) {
|
|
228
208
|
return input.subscribe((envelope) => {
|
|
229
|
-
// Another protocol on a shared event channel is not ours to judge
|
|
230
|
-
|
|
231
|
-
|
|
209
|
+
// Another protocol on a shared event channel is not ours to judge; an
|
|
210
|
+
// envelope without a protocol is malformed and must reach the
|
|
211
|
+
// validator, as in the Apps in Toss provider.
|
|
212
|
+
const protocol = typeof envelope === 'object' && envelope !== null
|
|
213
|
+
? envelope.protocol
|
|
214
|
+
: undefined;
|
|
215
|
+
if (typeof protocol === 'string' && protocol !== bridgeCommerceEventProtocol) {
|
|
232
216
|
return;
|
|
233
217
|
}
|
|
234
218
|
let event;
|
|
235
219
|
try {
|
|
236
|
-
event =
|
|
220
|
+
event = assertCommerceEventEnvelope(envelope);
|
|
237
221
|
}
|
|
238
222
|
catch (error) {
|
|
239
223
|
listener({
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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, createDegradedCommerceCapabilities,
|
|
5
|
-
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, type
|
|
4
|
+
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, type CapacitorCommerceServiceProvider, type CapacitorProviderCapabilitySnapshot, type CapacitorProviderRegistry, type CapacitorServiceProvider, type NativeBridge, } from './providers.js';
|
|
5
|
+
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, type CommerceProviderBridge, type CreateBridgeCommerceProviderInput, type CreateCommerceProviderBridgeInput, type CreateCommerceServiceProviderInput, } from './commerce-bridge.js';
|
|
6
6
|
export { createStoreCommerceProvider, recoverableErrorCodes, type CreateStoreCommerceProviderInput, type StoreCommerceCapabilityOverrides, type StoreCommerceProductMapping, type StoreCommerceProvider, } from './store-commerce-provider.js';
|
|
7
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';
|
|
8
8
|
export { createCapacitorAppEvents, type CapacitorAppEventsApi, type CapacitorIncomingUrlKind, type CapacitorVisibilitySource, } from './app-events.js';
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
|
-
import { decodeBridgeCredentialLoadData, decodeBridgeStorageLoadData, } from '@mpgd/bridge';
|
|
1
|
+
import { bridgeCommerceEventProtocol, decodeBridgeCredentialLoadData, decodeBridgeStorageLoadData, } from '@mpgd/bridge';
|
|
2
2
|
import { CapacitorGameServices } from '@mpgd/capacitor-game-services';
|
|
3
3
|
import { PlatformOperationError, } from '@mpgd/platform';
|
|
4
|
-
import {
|
|
4
|
+
import { withShareIntentCheck } from '@mpgd/platform/routes';
|
|
5
5
|
import { createCapacitorAppEvents, } from './app-events.js';
|
|
6
6
|
import { createBridgeCommerceProvider } from './commerce-bridge.js';
|
|
7
7
|
import { assertProviderResponseData, createCapacitorProviderRegistry, } from './providers.js';
|
|
8
|
+
import { assertNotificationsV2Served, externalActivityMethods } from './request-policy.js';
|
|
8
9
|
import { createCapacitorViewport } from './viewport.js';
|
|
9
|
-
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities,
|
|
10
|
+
export { createCapacitorProviderRegistry, createDegradedCommerceCapabilities, } from './providers.js';
|
|
10
11
|
export { assertCommerceBridgeResult, commerceV2InvalidPayloadBridgeErrorCode, commerceV2MethodUnsupportedBridgeErrorCode, createBridgeCommerceProvider, createCommerceProviderBridge, createCommerceServiceProvider, } from './commerce-bridge.js';
|
|
11
12
|
export { createStoreCommerceProvider, recoverableErrorCodes, } from './store-commerce-provider.js';
|
|
12
13
|
export { defaultStoreSdkErrorCodeMap, normalizeStoreSdkError, } from './store-sdk.js';
|
|
13
14
|
export { createCapacitorAppEvents, } from './app-events.js';
|
|
14
15
|
export { CapacitorNativeHttpError, createCapacitorNativeJsonTransport, } from './native-http.js';
|
|
15
16
|
export { createCapacitorViewport, } from './viewport.js';
|
|
16
|
-
const externalActivityMethods = new Set([
|
|
17
|
-
'commerce.purchase',
|
|
18
|
-
'commerce.v2.requestPurchase',
|
|
19
|
-
'ads.showRewarded',
|
|
20
|
-
'ads.showInterstitial',
|
|
21
|
-
'identity.requestUpgrade',
|
|
22
|
-
'notifications.requestSubscription',
|
|
23
|
-
]);
|
|
24
17
|
function isRecord(value) {
|
|
25
18
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
19
|
}
|
|
@@ -59,15 +52,28 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
59
52
|
const adProvider = providers.byMethod.get('ads.showRewarded')?.adProvider
|
|
60
53
|
?? providers.byMethod.get('ads.showInterstitial')?.adProvider;
|
|
61
54
|
const commerceProvider = providers.commerce;
|
|
62
|
-
// The
|
|
63
|
-
// provider method, so the bridge envelope, response validation, and
|
|
64
|
-
// activity bookkeeping are exercised in process exactly as over a
|
|
65
|
-
|
|
55
|
+
// The commerce provider is served through the same request path as every
|
|
56
|
+
// other provider method, so the bridge envelope, response validation, and
|
|
57
|
+
// external activity bookkeeping are exercised in process exactly as over a
|
|
58
|
+
// transport.
|
|
59
|
+
const commerce = commerceProvider === undefined
|
|
66
60
|
? undefined
|
|
67
61
|
: createBridgeCommerceProvider({
|
|
68
62
|
store: commerceProvider.commerce.store,
|
|
69
63
|
request: (method, payload) => request(method, payload),
|
|
70
|
-
|
|
64
|
+
// The provider's channel (`NativeBridge`) may carry other protocols.
|
|
65
|
+
// Only an envelope that names another protocol is dropped; anything
|
|
66
|
+
// else, a malformed envelope included, reaches the commerce
|
|
67
|
+
// provider's validator.
|
|
68
|
+
subscribe: (listener) => commerceProvider.bridge.subscribe((envelope) => {
|
|
69
|
+
const protocol = typeof envelope === 'object' && envelope !== null
|
|
70
|
+
? envelope.protocol
|
|
71
|
+
: undefined;
|
|
72
|
+
if (typeof protocol === 'string' && protocol !== bridgeCommerceEventProtocol) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
listener(envelope);
|
|
76
|
+
}),
|
|
71
77
|
});
|
|
72
78
|
const lifecycle = createCapacitorAppEvents({
|
|
73
79
|
target: input.target,
|
|
@@ -100,6 +106,9 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
100
106
|
},
|
|
101
107
|
};
|
|
102
108
|
async function request(method, payload, leaderboardRoute) {
|
|
109
|
+
// Before any pause: an unserved notifications v2 method never reaches the
|
|
110
|
+
// base plugin, which speaks no v2.
|
|
111
|
+
assertNotificationsV2Served(method, providers.byMethod.has(method));
|
|
103
112
|
const releaseExternalActivity = externalActivityMethods.has(method)
|
|
104
113
|
? lifecycle.beginExternalActivity?.()
|
|
105
114
|
: undefined;
|
|
@@ -110,29 +119,14 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
110
119
|
? undefined
|
|
111
120
|
: providers.byMethod.get(method);
|
|
112
121
|
if (provider !== undefined) {
|
|
113
|
-
let productType;
|
|
114
|
-
if (method === 'commerce.purchase'
|
|
115
|
-
&& provider.features.includes('nativeIap')
|
|
116
|
-
&& provider.features.includes('subscriptionIap')) {
|
|
117
|
-
// The caller supplies only an ID. Resolve its provider-owned type before
|
|
118
|
-
// selecting a store route; a forged or unknown ID must not bypass it.
|
|
119
|
-
const products = await request('commerce.getProducts', {});
|
|
120
|
-
const productId = isRecord(payload) ? payload.productId : undefined;
|
|
121
|
-
const matches = products.filter((product) => product.id === productId);
|
|
122
|
-
if (matches.length !== 1) {
|
|
123
|
-
throw new PlatformOperationError({ code: 'NATIVE_PROVIDER_PRODUCT_UNKNOWN' });
|
|
124
|
-
}
|
|
125
|
-
productType = matches[0]?.type;
|
|
126
|
-
}
|
|
127
122
|
try {
|
|
128
|
-
await providers.assertMethodReady(method, payload
|
|
123
|
+
await providers.assertMethodReady(method, payload);
|
|
129
124
|
const data = await sendRequest(method, payload, provider);
|
|
130
125
|
if (releaseExternalActivity !== undefined
|
|
131
126
|
&& method === 'commerce.v2.requestPurchase'
|
|
132
127
|
&& isRecord(data) && data.outcome === 'launched') {
|
|
133
128
|
// The store UI is still open: stay paused until this product's
|
|
134
|
-
// purchase event arrives or the safety timeout ends.
|
|
135
|
-
// has no event to wait for and releases when its request returns.
|
|
129
|
+
// purchase event arrives or the safety timeout ends.
|
|
136
130
|
holdActivity = true;
|
|
137
131
|
holdLaunchedPurchaseActivity(releaseExternalActivity, isRecord(payload) ? payload.logicalId : undefined);
|
|
138
132
|
}
|
|
@@ -179,7 +173,13 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
179
173
|
if (commerceProvider === undefined) {
|
|
180
174
|
return;
|
|
181
175
|
}
|
|
182
|
-
const subscription = commerceProvider.bridge.subscribe((
|
|
176
|
+
const subscription = commerceProvider.bridge.subscribe((envelope) => {
|
|
177
|
+
// Only this purchase's commerce events end the hold; the channel may
|
|
178
|
+
// carry other protocols.
|
|
179
|
+
if (envelope.protocol !== bridgeCommerceEventProtocol) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const { event } = envelope;
|
|
183
183
|
if (event.type === 'subscriptionUpdated') {
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
@@ -245,28 +245,29 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
245
245
|
target: input.target,
|
|
246
246
|
async getCapabilities() {
|
|
247
247
|
const base = await request('runtime.getCapabilities', {});
|
|
248
|
+
// A native shell built before the commerce v2 removals may still send the
|
|
249
|
+
// retired boolean flags; the commerce block is the only commerce source.
|
|
250
|
+
const { nativeIap: _retiredNativeIap, subscriptionIap: _retiredSubscriptionIap, ...native } = (isRecord(base) ? base : {});
|
|
248
251
|
// One bounded registry read serves the feature states and the commerce
|
|
249
252
|
// block, so a stalled or invalid provider degrades the snapshot instead
|
|
250
|
-
// of rejecting it, and
|
|
251
|
-
|
|
253
|
+
// of rejecting it, and a missing or invalid native `commerce` answer is
|
|
254
|
+
// always replaced by the provider's block or the unsupported block.
|
|
255
|
+
const { availability: states, commerce: commerceCapabilities } = await providers.getCapabilitySnapshot();
|
|
252
256
|
const rewardedAds = states.rewardedAds === 'available';
|
|
253
257
|
const interstitialAds = states.interstitialAds === 'available';
|
|
254
258
|
const bannerAds = states.bannerAds === 'available';
|
|
255
|
-
const legacy = commerce === undefined ? undefined : deriveLegacyCommerceCapabilities(commerce);
|
|
256
259
|
return {
|
|
257
|
-
...
|
|
258
|
-
nativeIap: legacy?.nativeIap ?? states.nativeIap === 'available',
|
|
259
|
-
subscriptionIap: legacy?.subscriptionIap ?? states.subscriptionIap === 'available',
|
|
260
|
+
...native,
|
|
260
261
|
nativeAds: rewardedAds || interstitialAds || bannerAds,
|
|
261
262
|
rewardedAds,
|
|
262
263
|
interstitialAds,
|
|
263
264
|
bannerAds,
|
|
264
265
|
nativeLeaderboard: states.nativeLeaderboard === 'available',
|
|
265
266
|
providerAvailability: states,
|
|
266
|
-
|
|
267
|
+
commerce: commerceCapabilities,
|
|
267
268
|
};
|
|
268
269
|
},
|
|
269
|
-
...(
|
|
270
|
+
...(commerce === undefined ? {} : { commerce }),
|
|
270
271
|
identity: {
|
|
271
272
|
getPlayer: () => request('identity.getPlayer', {}),
|
|
272
273
|
getSession: () => request('identity.getSession', {}),
|
|
@@ -276,20 +277,16 @@ export function createCapacitorPlatformGateway(input) {
|
|
|
276
277
|
getLaunchIntent: () => request('presentation.getLaunchIntent', {}),
|
|
277
278
|
requestGameSurface: (payload) => request('presentation.requestGameSurface', payload),
|
|
278
279
|
},
|
|
279
|
-
|
|
280
|
+
// The bridge's host may build a share URL from the link, so it only ever
|
|
281
|
+
// sees an intent the ShareIntent contract accepts, with the link normalized.
|
|
282
|
+
sharing: withShareIntentCheck({
|
|
280
283
|
share: (payload) => request('share.share', payload),
|
|
281
284
|
readInboundShare: () => request('share.readInboundShare', {}),
|
|
282
|
-
},
|
|
285
|
+
}),
|
|
283
286
|
notifications: {
|
|
284
287
|
getStatus: (topic) => request('notifications.getStatus', { topic }),
|
|
285
288
|
requestSubscription: (topic) => request('notifications.requestSubscription', { topic }),
|
|
286
289
|
},
|
|
287
|
-
commerce: {
|
|
288
|
-
getProducts: () => request('commerce.getProducts', {}),
|
|
289
|
-
purchase: (payload) => request('commerce.purchase', payload),
|
|
290
|
-
restore: () => request('commerce.restore', {}),
|
|
291
|
-
getEntitlements: () => request('commerce.getEntitlements', {}),
|
|
292
|
-
},
|
|
293
290
|
ads: {
|
|
294
291
|
...(adProvider === undefined ? {} : { provider: adProvider }),
|
|
295
292
|
preload: (payload) => request('ads.preload', payload),
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type BridgeEvent, type BridgeMethod, type BridgeRequest, type BridgeResponse } from '@mpgd/bridge';
|
|
2
2
|
import type { AdProvider } from '@mpgd/platform/ads';
|
|
3
|
-
import { type CommerceCapabilities, type PlatformProviderAvailability, type PlatformProviderFeature
|
|
3
|
+
import { type CommerceCapabilities, type PlatformProviderAvailability, type PlatformProviderFeature } from '@mpgd/platform';
|
|
4
4
|
import { type CommerceProvider } from '@mpgd/platform/commerce';
|
|
5
5
|
export interface NativeBridge {
|
|
6
6
|
request(input: BridgeRequest): Promise<BridgeResponse>;
|
|
7
7
|
/**
|
|
8
|
-
* Optional
|
|
9
|
-
*
|
|
8
|
+
* Optional event delivery in the bridge envelopes, told apart by `protocol`
|
|
9
|
+
* (`{ protocol: 'mpgd.commerce.v2', sequence, event }` from store
|
|
10
|
+
* providers). The base gateway works without it.
|
|
10
11
|
*/
|
|
11
12
|
subscribe?(listener: (event: BridgeEvent) => void): () => void;
|
|
12
13
|
}
|
|
@@ -20,23 +21,28 @@ export interface CapacitorServiceProvider {
|
|
|
20
21
|
readonly id: string;
|
|
21
22
|
/**
|
|
22
23
|
* Serves the declared `methods`. For a provider with `commerce`, it also
|
|
23
|
-
* serves the
|
|
24
|
+
* serves the six `commerce.v2.*` methods and must implement `subscribe`;
|
|
24
25
|
* `createCommerceServiceProvider` builds one from a `CommerceProvider`.
|
|
25
26
|
*/
|
|
26
27
|
readonly bridge: NativeBridge;
|
|
28
|
+
/**
|
|
29
|
+
* The non-commerce features this provider serves. A provider with `commerce`
|
|
30
|
+
* may declare none: commerce readiness is the provider's own capability
|
|
31
|
+
* block, not a feature.
|
|
32
|
+
*/
|
|
27
33
|
readonly features: readonly PlatformProviderFeature[];
|
|
28
34
|
/**
|
|
29
|
-
* Declared
|
|
30
|
-
* `commerce.*` method: the
|
|
31
|
-
*
|
|
35
|
+
* Declared bridge methods, owned through `features`. A provider with
|
|
36
|
+
* `commerce` declares no `commerce.v2.*` method: the six are implied and
|
|
37
|
+
* owned by the commerce provider, so its own list may be empty.
|
|
32
38
|
*/
|
|
33
39
|
readonly methods: readonly BridgeMethod[];
|
|
34
40
|
/** Optional native event surface. The base adapter never imports its SDK. */
|
|
35
41
|
readonly adProvider?: AdProvider;
|
|
36
42
|
/**
|
|
37
|
-
* Optional commerce
|
|
38
|
-
* `getCapabilities()` is the
|
|
39
|
-
*
|
|
43
|
+
* Optional commerce provider (`createStoreCommerceProvider`). Its
|
|
44
|
+
* `getCapabilities()` is the gateway's commerce block; `getAvailability`
|
|
45
|
+
* covers the declared `features`.
|
|
40
46
|
*/
|
|
41
47
|
readonly commerce?: CommerceProvider;
|
|
42
48
|
getAvailability(): Promise<Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>>;
|
|
@@ -49,13 +55,14 @@ export interface CapacitorCommerceServiceProvider extends CapacitorServiceProvid
|
|
|
49
55
|
export interface CapacitorProviderCapabilitySnapshot {
|
|
50
56
|
readonly availability: Readonly<Record<PlatformProviderFeature, PlatformProviderAvailability>>;
|
|
51
57
|
/**
|
|
52
|
-
*
|
|
53
|
-
* `getCapabilities()` answer
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
*
|
|
58
|
+
* Always present, and a fresh copy on every read: the registered commerce
|
|
59
|
+
* provider's validated `getCapabilities()` answer, the degraded block
|
|
60
|
+
* (`availability: 'temporarily-unavailable'`, nothing sold) when that bounded
|
|
61
|
+
* read stalled, failed, or returned a shape the contract rejects, or
|
|
62
|
+
* `createUnsupportedCommerceCapabilities()` when no commerce provider is
|
|
63
|
+
* registered.
|
|
57
64
|
*/
|
|
58
|
-
readonly commerce
|
|
65
|
+
readonly commerce: CommerceCapabilities;
|
|
59
66
|
}
|
|
60
67
|
export interface CapacitorProviderRegistry {
|
|
61
68
|
readonly byMethod: ReadonlyMap<BridgeMethod, CapacitorServiceProvider>;
|
|
@@ -63,16 +70,10 @@ export interface CapacitorProviderRegistry {
|
|
|
63
70
|
readonly commerce?: CapacitorCommerceServiceProvider;
|
|
64
71
|
/** Feature readiness plus the commerce block, from one bounded read per provider. */
|
|
65
72
|
getCapabilitySnapshot(): Promise<CapacitorProviderCapabilitySnapshot>;
|
|
66
|
-
assertMethodReady(method: BridgeMethod, payload?: unknown
|
|
73
|
+
assertMethodReady(method: BridgeMethod, payload?: unknown): Promise<void>;
|
|
67
74
|
getAvailability(): Promise<Readonly<Record<PlatformProviderFeature, PlatformProviderAvailability>>>;
|
|
68
75
|
}
|
|
69
76
|
export declare function createCapacitorProviderRegistry(input: readonly CapacitorServiceProvider[]): CapacitorProviderRegistry;
|
|
70
77
|
export declare function assertProviderResponseData(method: BridgeMethod, data: unknown): void;
|
|
71
78
|
/** The commerce block a gateway reports while its provider cannot be read. */
|
|
72
79
|
export declare function createDegradedCommerceCapabilities(): CommerceCapabilities;
|
|
73
|
-
/**
|
|
74
|
-
* Readiness of the commerce features a v2 provider declares, from its own
|
|
75
|
-
* capability declaration: a feature it does not sell is `unsupported`, and a
|
|
76
|
-
* read that failed or stalled is `temporarily-unavailable`.
|
|
77
|
-
*/
|
|
78
|
-
export declare function deriveCommerceFeatureStates(capabilities: CommerceCapabilities | undefined, features: readonly PlatformProviderFeature[]): Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>;
|
package/dist/providers.js
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import { bridgeCommerceV2Methods, isBridgeCommerceV2Method, } from '@mpgd/bridge';
|
|
1
|
+
import { bridgeCommerceV2Methods, isBridgeCommerceV2Method, isBridgeNotificationsV2Method, } from '@mpgd/bridge';
|
|
2
2
|
import { isAdMobClientRewardEvidence } from '@mpgd/game-services/admob-client-reward';
|
|
3
3
|
import { PlatformOperationError, } from '@mpgd/platform';
|
|
4
4
|
import { createUnsupportedCommerceCapabilities, isCommerceCapabilities, } from '@mpgd/platform/commerce';
|
|
5
5
|
import { assertCommerceBridgeResult } from './commerce-bridge.js';
|
|
6
6
|
const featureMethods = {
|
|
7
|
-
nativeIap: [
|
|
8
|
-
'commerce.getProducts',
|
|
9
|
-
'commerce.purchase',
|
|
10
|
-
'commerce.restore',
|
|
11
|
-
'commerce.getEntitlements',
|
|
12
|
-
],
|
|
13
|
-
subscriptionIap: [
|
|
14
|
-
'commerce.getProducts',
|
|
15
|
-
'commerce.purchase',
|
|
16
|
-
'commerce.restore',
|
|
17
|
-
'commerce.getEntitlements',
|
|
18
|
-
],
|
|
19
7
|
rewardedAds: ['ads.preload', 'ads.showRewarded'],
|
|
20
8
|
interstitialAds: ['ads.preload', 'ads.showInterstitial'],
|
|
21
9
|
bannerAds: ['ads.mountBanner', 'ads.unmountBanner'],
|
|
@@ -25,12 +13,7 @@ const featureMethods = {
|
|
|
25
13
|
};
|
|
26
14
|
const providerFeatures = Object.keys(featureMethods);
|
|
27
15
|
const providerMethodSet = new Set(Object.values(featureMethods).flat());
|
|
28
|
-
const
|
|
29
|
-
const v1CommerceMethods = featureMethods.nativeIap;
|
|
30
|
-
const impliedCommerceMethods = [
|
|
31
|
-
...v1CommerceMethods,
|
|
32
|
-
...bridgeCommerceV2Methods,
|
|
33
|
-
];
|
|
16
|
+
const impliedCommerceMethods = bridgeCommerceV2Methods;
|
|
34
17
|
const availabilitySet = new Set([
|
|
35
18
|
'unsupported',
|
|
36
19
|
'configuration-required',
|
|
@@ -55,8 +38,8 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
55
38
|
throw new Error(`Capacitor provider ID is empty or registered twice: ${provider.id}`);
|
|
56
39
|
}
|
|
57
40
|
ids.add(provider.id);
|
|
58
|
-
if (provider.
|
|
59
|
-
|
|
41
|
+
if (provider.commerce === undefined
|
|
42
|
+
&& (provider.features.length === 0 || provider.methods.length === 0)) {
|
|
60
43
|
throw new Error(`Capacitor provider ${provider.id} must declare features and methods.`);
|
|
61
44
|
}
|
|
62
45
|
const methods = new Set(provider.methods);
|
|
@@ -67,16 +50,18 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
67
50
|
if (isBridgeCommerceV2Method(method)) {
|
|
68
51
|
// A v2 provider is registered through `commerce`; its bridge methods are implied.
|
|
69
52
|
throw new Error(`Capacitor provider ${provider.id} cannot declare commerce v2 method ${method}; `
|
|
70
|
-
+ 'register a
|
|
71
|
-
+ 'a declared method.');
|
|
53
|
+
+ 'register a commerce provider through `commerce` instead, commerce v2 is '
|
|
54
|
+
+ 'unavailable as a declared method.');
|
|
55
|
+
}
|
|
56
|
+
if (isBridgeNotificationsV2Method(method)) {
|
|
57
|
+
// A notifications v2 provider gets its own registry surface (P2); a
|
|
58
|
+
// declared method would bypass the pass-through readiness rule.
|
|
59
|
+
throw new Error(`Capacitor provider ${provider.id} cannot declare notifications v2 method ${method}; `
|
|
60
|
+
+ 'notifications v2 is unavailable as a declared method.');
|
|
72
61
|
}
|
|
73
62
|
if (!providerMethodSet.has(method)) {
|
|
74
63
|
throw new Error(`Capacitor provider ${provider.id} cannot replace base method ${method}.`);
|
|
75
64
|
}
|
|
76
|
-
if (provider.commerce !== undefined && v1CommerceMethods.includes(method)) {
|
|
77
|
-
throw new Error(`Capacitor provider ${provider.id} cannot declare both a commerce v2 provider and `
|
|
78
|
-
+ `v1 method ${method}; the registry serves v1 callers from the v2 provider.`);
|
|
79
|
-
}
|
|
80
65
|
}
|
|
81
66
|
const implied = provider.commerce === undefined ? [] : impliedCommerceMethods;
|
|
82
67
|
for (const method of [...methods, ...implied]) {
|
|
@@ -85,16 +70,11 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
85
70
|
}
|
|
86
71
|
}
|
|
87
72
|
const owned = new Set([...methods, ...implied]);
|
|
88
|
-
if (provider.commerce !== undefined) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Without the event surface a purchase that completes after the store
|
|
94
|
-
// UI closed would never reach the reconciler.
|
|
95
|
-
throw new Error(`Capacitor provider ${provider.id} declares a commerce v2 provider but its bridge has no `
|
|
96
|
-
+ 'subscribe; build it with createCommerceServiceProvider.');
|
|
97
|
-
}
|
|
73
|
+
if (provider.commerce !== undefined && provider.bridge.subscribe === undefined) {
|
|
74
|
+
// Without the event surface a purchase that completes after the store
|
|
75
|
+
// UI closed would never reach the reconciler.
|
|
76
|
+
throw new Error(`Capacitor provider ${provider.id} declares a commerce provider but its bridge has no `
|
|
77
|
+
+ 'subscribe; build it with createCommerceServiceProvider.');
|
|
98
78
|
}
|
|
99
79
|
for (const feature of provider.features) {
|
|
100
80
|
const required = featureMethods[feature];
|
|
@@ -117,26 +97,27 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
117
97
|
commerce = provider;
|
|
118
98
|
}
|
|
119
99
|
}
|
|
120
|
-
const
|
|
100
|
+
const readFeatureStates = async (provider) => {
|
|
121
101
|
const states = {};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
: provider.features.filter((feature) => commerceFeatures.includes(feature));
|
|
125
|
-
const bridged = provider.features.filter((feature) => !commerceOwned.includes(feature));
|
|
126
|
-
let commerceBlock;
|
|
127
|
-
if (provider.commerce !== undefined && commerceOwned.length > 0) {
|
|
128
|
-
commerceBlock = await readCommerceCapabilities(provider.commerce);
|
|
129
|
-
Object.assign(states, deriveCommerceFeatureStates(commerceBlock, commerceOwned));
|
|
102
|
+
if (provider.features.length === 0) {
|
|
103
|
+
return states;
|
|
130
104
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
: 'temporarily-unavailable';
|
|
138
|
-
}
|
|
105
|
+
const reported = await readWithTimeout(Promise.resolve().then(() => provider.getAvailability()));
|
|
106
|
+
for (const feature of provider.features) {
|
|
107
|
+
const state = isRecord(reported) ? reported[feature] : undefined;
|
|
108
|
+
states[feature] = availabilitySet.has(state)
|
|
109
|
+
? state
|
|
110
|
+
: 'temporarily-unavailable';
|
|
139
111
|
}
|
|
112
|
+
return states;
|
|
113
|
+
};
|
|
114
|
+
const readProviderStates = async (provider) => {
|
|
115
|
+
const [states, commerceBlock] = await Promise.all([
|
|
116
|
+
readFeatureStates(provider),
|
|
117
|
+
provider.commerce === undefined
|
|
118
|
+
? Promise.resolve(undefined)
|
|
119
|
+
: readCommerceCapabilities(provider.commerce),
|
|
120
|
+
]);
|
|
140
121
|
return { states, ...(commerceBlock === undefined ? {} : { commerce: commerceBlock }) };
|
|
141
122
|
};
|
|
142
123
|
const getCapabilitySnapshot = async () => {
|
|
@@ -150,15 +131,15 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
150
131
|
Object.assign(states, snapshot.states);
|
|
151
132
|
commerceBlock = snapshot.commerce ?? commerceBlock;
|
|
152
133
|
}
|
|
153
|
-
if (commerce
|
|
154
|
-
|
|
155
|
-
|
|
134
|
+
if (commerce === undefined) {
|
|
135
|
+
commerceBlock = createUnsupportedCommerceCapabilities();
|
|
136
|
+
}
|
|
137
|
+
else if (commerceBlock === undefined) {
|
|
138
|
+
// One bounded read serves the block, so a stalled, failed, or invalid
|
|
139
|
+
// provider degrades it without rejecting the snapshot.
|
|
156
140
|
commerceBlock = createDegradedCommerceCapabilities();
|
|
157
141
|
}
|
|
158
|
-
return {
|
|
159
|
-
availability: states,
|
|
160
|
-
...(commerceBlock === undefined ? {} : { commerce: commerceBlock }),
|
|
161
|
-
};
|
|
142
|
+
return { availability: states, commerce: commerceBlock };
|
|
162
143
|
};
|
|
163
144
|
const getAvailability = async () => (await getCapabilitySnapshot()).availability;
|
|
164
145
|
return {
|
|
@@ -166,14 +147,17 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
166
147
|
...(commerce === undefined ? {} : { commerce }),
|
|
167
148
|
getAvailability,
|
|
168
149
|
getCapabilitySnapshot,
|
|
169
|
-
async assertMethodReady(method, payload
|
|
150
|
+
async assertMethodReady(method, payload) {
|
|
170
151
|
const provider = byMethod.get(method);
|
|
171
152
|
if (provider === undefined) {
|
|
172
153
|
return;
|
|
173
154
|
}
|
|
174
|
-
if (isBridgeCommerceV2Method(method)) {
|
|
175
|
-
//
|
|
176
|
-
// any store UI opens;
|
|
155
|
+
if (isBridgeCommerceV2Method(method) || isBridgeNotificationsV2Method(method)) {
|
|
156
|
+
// A v2 provider reads its own `availability` and answers with it
|
|
157
|
+
// (commerce fails closed before any store UI opens; notifications
|
|
158
|
+
// answer `unsupported` or `action-required` states and outcomes), so
|
|
159
|
+
// the registry adds no second, stale snapshot that would turn those
|
|
160
|
+
// answers into errors.
|
|
177
161
|
return;
|
|
178
162
|
}
|
|
179
163
|
if (method === 'ads.unmountBanner') {
|
|
@@ -181,14 +165,10 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
181
165
|
// its serving state becomes unavailable.
|
|
182
166
|
return;
|
|
183
167
|
}
|
|
184
|
-
// An unrelated provider must not delay
|
|
185
|
-
|
|
168
|
+
// An unrelated provider must not delay an ad or identity call, and neither
|
|
169
|
+
// may its commerce capability read.
|
|
170
|
+
const states = await readFeatureStates(provider);
|
|
186
171
|
let relevant = provider.features.filter((feature) => featureMethods[feature].includes(method));
|
|
187
|
-
if (method === 'commerce.purchase' && relevant.length > 1) {
|
|
188
|
-
relevant = relevant.filter((feature) => feature === (productType === 'subscription' ? 'subscriptionIap'
|
|
189
|
-
: productType === 'consumable' || productType === 'non_consumable'
|
|
190
|
-
? 'nativeIap' : undefined));
|
|
191
|
-
}
|
|
192
172
|
const preloadFormat = method === 'ads.preload' && isRecord(payload)
|
|
193
173
|
? payload.format
|
|
194
174
|
: undefined;
|
|
@@ -201,15 +181,9 @@ export function createCapacitorProviderRegistry(input) {
|
|
|
201
181
|
relevant = feature === undefined ? [] : relevant.filter((item) => item === feature);
|
|
202
182
|
}
|
|
203
183
|
if (relevant.length === 0) {
|
|
204
|
-
throw new PlatformOperationError({
|
|
205
|
-
code: method === 'commerce.purchase'
|
|
206
|
-
? 'NATIVE_PROVIDER_PRODUCT_TYPE_REQUIRED'
|
|
207
|
-
: 'NATIVE_PROVIDER_UNSUPPORTED',
|
|
208
|
-
});
|
|
184
|
+
throw new PlatformOperationError({ code: 'NATIVE_PROVIDER_UNSUPPORTED' });
|
|
209
185
|
}
|
|
210
|
-
const ready =
|
|
211
|
-
|| ((method === 'commerce.restore' || method === 'commerce.getEntitlements')
|
|
212
|
-
&& relevant.length > 1)
|
|
186
|
+
const ready = method === 'ads.preload' && preloadFormat === undefined
|
|
213
187
|
? relevant.every((feature) => states[feature] === 'available')
|
|
214
188
|
: relevant.some((feature) => states[feature] === 'available');
|
|
215
189
|
if (ready) {
|
|
@@ -288,41 +262,12 @@ async function readWithTimeout(read) {
|
|
|
288
262
|
}
|
|
289
263
|
}
|
|
290
264
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Readiness of the commerce features a v2 provider declares, from its own
|
|
293
|
-
* capability declaration: a feature it does not sell is `unsupported`, and a
|
|
294
|
-
* read that failed or stalled is `temporarily-unavailable`.
|
|
295
|
-
*/
|
|
296
|
-
export function deriveCommerceFeatureStates(capabilities, features) {
|
|
297
|
-
const states = {};
|
|
298
|
-
const availability = capabilities === undefined
|
|
299
|
-
|| !availabilitySet.has(capabilities.availability)
|
|
300
|
-
? 'temporarily-unavailable'
|
|
301
|
-
: capabilities.availability;
|
|
302
|
-
for (const feature of features) {
|
|
303
|
-
if (feature === 'nativeIap') {
|
|
304
|
-
states.nativeIap = capabilities !== undefined
|
|
305
|
-
&& !(capabilities.purchase.consumable || capabilities.purchase.durable)
|
|
306
|
-
? 'unsupported'
|
|
307
|
-
: availability;
|
|
308
|
-
}
|
|
309
|
-
else if (feature === 'subscriptionIap') {
|
|
310
|
-
states.subscriptionIap = capabilities !== undefined && !capabilities.purchase.subscription
|
|
311
|
-
? 'unsupported'
|
|
312
|
-
: availability;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return states;
|
|
316
|
-
}
|
|
317
265
|
function isRecord(value) {
|
|
318
266
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
319
267
|
}
|
|
320
268
|
function isString(value) {
|
|
321
269
|
return typeof value === 'string';
|
|
322
270
|
}
|
|
323
|
-
function isStringArray(value) {
|
|
324
|
-
return Array.isArray(value) && value.every(isString);
|
|
325
|
-
}
|
|
326
271
|
function isVoidData(value) {
|
|
327
272
|
return value === undefined || value === null || (isRecord(value) && Object.keys(value).length === 0);
|
|
328
273
|
}
|
|
@@ -336,47 +281,13 @@ function isValidProviderData(method, value) {
|
|
|
336
281
|
return false;
|
|
337
282
|
}
|
|
338
283
|
}
|
|
284
|
+
if (isBridgeNotificationsV2Method(method)) {
|
|
285
|
+
// P2 must validate these answers here, with `assertBridgeNotificationsV2Result`
|
|
286
|
+
// from `@mpgd/bridge/notifications`, when it lets a provider serve the
|
|
287
|
+
// methods. Until then every notifications v2 answer is rejected.
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
339
290
|
switch (method) {
|
|
340
|
-
case 'commerce.getProducts':
|
|
341
|
-
return Array.isArray(value) && value.every((product) => isRecord(product)
|
|
342
|
-
&& isString(product.id)
|
|
343
|
-
&& ['consumable', 'non_consumable', 'subscription'].includes(product.type)
|
|
344
|
-
&& isString(product.title)
|
|
345
|
-
&& isString(product.description)
|
|
346
|
-
&& isRecord(product.price)
|
|
347
|
-
&& isString(product.price.formatted)
|
|
348
|
-
&& isString(product.price.currencyCode));
|
|
349
|
-
case 'commerce.purchase':
|
|
350
|
-
return isRecord(value)
|
|
351
|
-
&& ['completed', 'cancelled', 'pending', 'failed'].includes(value.status)
|
|
352
|
-
&& isStringArray(value.entitlementIds)
|
|
353
|
-
&& (value.transactionId === undefined || isString(value.transactionId))
|
|
354
|
-
&& (value.authoritativeGrant === undefined || (value.status === 'completed'
|
|
355
|
-
&& isString(value.transactionId)
|
|
356
|
-
&& isRecord(value.authoritativeGrant)
|
|
357
|
-
&& isString(value.authoritativeGrant.ledgerEntryId)
|
|
358
|
-
&& value.authoritativeGrant.ledgerEntryId.length > 0
|
|
359
|
-
&& (value.authoritativeGrant.alreadyProcessed === undefined
|
|
360
|
-
|| typeof value.authoritativeGrant.alreadyProcessed === 'boolean')))
|
|
361
|
-
&& (value.evidence === undefined || isEvidence(value.evidence))
|
|
362
|
-
&& (value.diagnostic === undefined || isCommerceDiagnostic(value.diagnostic));
|
|
363
|
-
case 'commerce.restore':
|
|
364
|
-
return isRecord(value)
|
|
365
|
-
&& Array.isArray(value.restoredEntitlements)
|
|
366
|
-
&& value.restoredEntitlements.every(isEntitlement)
|
|
367
|
-
&& (value.settledPurchases === undefined || (Array.isArray(value.settledPurchases)
|
|
368
|
-
&& value.settledPurchases.every((settlement) => isRecord(settlement)
|
|
369
|
-
&& isString(settlement.transactionId)
|
|
370
|
-
&& isString(settlement.productId)
|
|
371
|
-
&& ['granted', 'refunded'].includes(settlement.status)
|
|
372
|
-
&& (settlement.ledgerEntryId === undefined || isString(settlement.ledgerEntryId))
|
|
373
|
-
&& (settlement.alreadyProcessed === undefined
|
|
374
|
-
|| typeof settlement.alreadyProcessed === 'boolean')
|
|
375
|
-
&& (settlement.idempotencyKey === undefined
|
|
376
|
-
|| isIdempotencyKey(settlement.idempotencyKey)))))
|
|
377
|
-
&& (value.diagnostic === undefined || isCommerceDiagnostic(value.diagnostic));
|
|
378
|
-
case 'commerce.getEntitlements':
|
|
379
|
-
return Array.isArray(value) && value.every(isEntitlement);
|
|
380
291
|
case 'ads.preload':
|
|
381
292
|
case 'ads.unmountBanner':
|
|
382
293
|
case 'leaderboard.open':
|
|
@@ -426,29 +337,6 @@ function isValidProviderData(method, value) {
|
|
|
426
337
|
return false;
|
|
427
338
|
}
|
|
428
339
|
}
|
|
429
|
-
function isEntitlement(value) {
|
|
430
|
-
return isRecord(value)
|
|
431
|
-
&& isString(value.id)
|
|
432
|
-
&& ['purchase', 'promotion', 'admin'].includes(value.source)
|
|
433
|
-
&& isString(value.grantedAt)
|
|
434
|
-
&& (value.expiresAt === undefined || isString(value.expiresAt));
|
|
435
|
-
}
|
|
436
|
-
/** Mirrors the stable, non-sensitive code format of `PlatformOperationError`. */
|
|
437
|
-
function isDiagnosticCode(value) {
|
|
438
|
-
return isString(value) && value.length <= 128 && /^[A-Z][A-Z0-9_:-]*$/u.test(value);
|
|
439
|
-
}
|
|
440
|
-
function isCommerceDiagnostic(value) {
|
|
441
|
-
return isRecord(value)
|
|
442
|
-
&& isDiagnosticCode(value.code)
|
|
443
|
-
&& typeof value.retryable === 'boolean'
|
|
444
|
-
&& (value.providerCode === undefined || isDiagnosticCode(value.providerCode));
|
|
445
|
-
}
|
|
446
|
-
function isIdempotencyKey(value) {
|
|
447
|
-
return isString(value)
|
|
448
|
-
&& value.length > 0
|
|
449
|
-
&& value.length <= 256
|
|
450
|
-
&& !/[\p{Cc}\p{Cf}]/u.test(value);
|
|
451
|
-
}
|
|
452
340
|
function isEvidence(value) {
|
|
453
341
|
return isRecord(value)
|
|
454
342
|
&& isString(value.schema)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BridgeMethod } from '@mpgd/bridge';
|
|
2
|
+
/**
|
|
3
|
+
* Methods that put a native UI over the game, so the gateway pauses the game
|
|
4
|
+
* (`beginExternalActivity`) while one runs: a store sheet, an ad, a sign-in,
|
|
5
|
+
* a permission dialog, or the system settings that
|
|
6
|
+
* `notifications.v2.openSettings` leaves for (notifications RFC "Provider
|
|
7
|
+
* profiles", Capacitor).
|
|
8
|
+
*/
|
|
9
|
+
export declare const externalActivityMethods: ReadonlySet<BridgeMethod>;
|
|
10
|
+
/**
|
|
11
|
+
* Fails closed for a notifications v2 method that no registered provider
|
|
12
|
+
* serves: the base game-services plugin speaks no notifications v2, so the
|
|
13
|
+
* request answers `NOTIFICATIONS_V2_UNAVAILABLE`, which is not retryable,
|
|
14
|
+
* without pausing the game or reaching the plugin.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertNotificationsV2Served(method: BridgeMethod, served: boolean): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isBridgeNotificationsV2Method, notificationsV2UnavailableBridgeErrorCode, } from '@mpgd/bridge';
|
|
2
|
+
import { PlatformOperationError } from '@mpgd/platform';
|
|
3
|
+
// How the gateway's request path treats a method before routing it. Internal:
|
|
4
|
+
// the package entry does not export this module.
|
|
5
|
+
/**
|
|
6
|
+
* Methods that put a native UI over the game, so the gateway pauses the game
|
|
7
|
+
* (`beginExternalActivity`) while one runs: a store sheet, an ad, a sign-in,
|
|
8
|
+
* a permission dialog, or the system settings that
|
|
9
|
+
* `notifications.v2.openSettings` leaves for (notifications RFC "Provider
|
|
10
|
+
* profiles", Capacitor).
|
|
11
|
+
*/
|
|
12
|
+
export const externalActivityMethods = new Set([
|
|
13
|
+
'commerce.v2.requestPurchase',
|
|
14
|
+
'ads.showRewarded',
|
|
15
|
+
'ads.showInterstitial',
|
|
16
|
+
'identity.requestUpgrade',
|
|
17
|
+
'notifications.requestSubscription',
|
|
18
|
+
'notifications.v2.requestPermission',
|
|
19
|
+
'notifications.v2.openSettings',
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* Fails closed for a notifications v2 method that no registered provider
|
|
23
|
+
* serves: the base game-services plugin speaks no notifications v2, so the
|
|
24
|
+
* request answers `NOTIFICATIONS_V2_UNAVAILABLE`, which is not retryable,
|
|
25
|
+
* without pausing the game or reaching the plugin.
|
|
26
|
+
*/
|
|
27
|
+
export function assertNotificationsV2Served(method, served) {
|
|
28
|
+
if (served || !isBridgeNotificationsV2Method(method)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
throw new PlatformOperationError({
|
|
32
|
+
code: notificationsV2UnavailableBridgeErrorCode,
|
|
33
|
+
message: `No Capacitor provider serves notifications v2; ${method} is unavailable.`,
|
|
34
|
+
retryable: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-capacitor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Capacitor platform adapter for mpgd Android and iOS shells.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,10 +32,6 @@
|
|
|
32
32
|
"./admob": {
|
|
33
33
|
"types": "./dist/admob.d.ts",
|
|
34
34
|
"default": "./dist/admob.js"
|
|
35
|
-
},
|
|
36
|
-
"./app-store-server": {
|
|
37
|
-
"types": "./dist/app-store-server.d.ts",
|
|
38
|
-
"default": "./dist/app-store-server.js"
|
|
39
35
|
}
|
|
40
36
|
},
|
|
41
37
|
"files": [
|
|
@@ -43,11 +39,11 @@
|
|
|
43
39
|
],
|
|
44
40
|
"dependencies": {
|
|
45
41
|
"@capacitor/app": "8.1.1",
|
|
46
|
-
"@mpgd/bridge": "0.
|
|
47
|
-
"@mpgd/game-services": "0.
|
|
48
|
-
"@mpgd/
|
|
49
|
-
"@mpgd/
|
|
50
|
-
"@mpgd/
|
|
42
|
+
"@mpgd/bridge": "0.13.0",
|
|
43
|
+
"@mpgd/capacitor-game-services": "0.7.0",
|
|
44
|
+
"@mpgd/platform": "0.23.0",
|
|
45
|
+
"@mpgd/target-config": "0.23.0",
|
|
46
|
+
"@mpgd/game-services": "0.24.0"
|
|
51
47
|
},
|
|
52
48
|
"peerDependencies": {
|
|
53
49
|
"@capacitor-community/admob": "^8.1.0",
|
|
@@ -64,7 +60,7 @@
|
|
|
64
60
|
"ttsc": "0.30.4",
|
|
65
61
|
"typescript": "7.0.2",
|
|
66
62
|
"vitest": "^4.0.0",
|
|
67
|
-
"@mpgd/game-runtime": "0.
|
|
63
|
+
"@mpgd/game-runtime": "0.7.0"
|
|
68
64
|
},
|
|
69
65
|
"main": "./dist/index.js",
|
|
70
66
|
"types": "./dist/index.d.ts",
|
|
@@ -1,12 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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;
|