@mpgd/cli 0.45.0 → 0.47.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/dist/capacitor-shell-starter.d.ts +57 -1
- package/dist/capacitor-shell-starter.js +311 -34
- package/dist/index.js +23 -5
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +613 -316
- package/package.json +17 -15
- package/templates/phaser-game/apps/target-ait/src/main.ts +17 -0
- package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +2 -0
- package/templates/phaser-game/apps/target-ait/vite.config.ts +27 -0
- package/templates/phaser-game/apps/target-devvit/README.md +8 -1
- package/templates/phaser-game/apps/target-devvit/package.json +2 -1
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +35 -1
- package/templates/phaser-game/package.json +2 -0
- package/templates/phaser-game/src/main.ts +8 -1
- package/templates/phaser-game/src/platform/buildGateways/ait.ts +9 -0
- package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +7 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +51 -2
- package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +51 -2
- package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +62 -26
- package/templates/phaser-game/src/platform/buildGateways/reddit.ts +36 -1
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +40 -2
- package/templates/phaser-game/src/platform/commerceAccountBinding.ts +79 -0
- package/templates/phaser-game/src/platform/commerceProducts.ts +69 -0
- package/templates/phaser-game/src/platform/gameServices.ts +22 -2
- package/templates/phaser-game/src/platform/monetizationJournal.ts +286 -0
- package/templates/phaser-game/src/platform/verse8Agent8Client.ts +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"undici": "^7.29.1",
|
|
59
59
|
"vite": "8.1.3",
|
|
60
60
|
"yauzl": "3.4.0",
|
|
61
|
-
"@mpgd/
|
|
62
|
-
"@mpgd/
|
|
63
|
-
"@mpgd/
|
|
61
|
+
"@mpgd/catalog": "0.9.3",
|
|
62
|
+
"@mpgd/phaser-assets": "0.9.0",
|
|
63
|
+
"@mpgd/target-config": "0.22.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.0.0",
|
|
@@ -68,17 +68,19 @@
|
|
|
68
68
|
"@types/yauzl": "3.4.0",
|
|
69
69
|
"ttsc": "0.30.4",
|
|
70
70
|
"typescript": "7.0.2",
|
|
71
|
-
"@mpgd/adapter-
|
|
72
|
-
"@mpgd/adapter-
|
|
73
|
-
"@mpgd/adapter-capacitor": "0.9.
|
|
74
|
-
"@mpgd/adapter-devvit": "0.
|
|
75
|
-
"@mpgd/
|
|
76
|
-
"@mpgd/analytics": "0.4.
|
|
77
|
-
"@mpgd/
|
|
78
|
-
"@mpgd/
|
|
79
|
-
"@mpgd/
|
|
80
|
-
"@mpgd/
|
|
81
|
-
"@mpgd/
|
|
71
|
+
"@mpgd/adapter-browser": "0.10.2",
|
|
72
|
+
"@mpgd/adapter-ait": "0.16.0",
|
|
73
|
+
"@mpgd/adapter-capacitor": "0.9.3",
|
|
74
|
+
"@mpgd/adapter-devvit": "0.11.0",
|
|
75
|
+
"@mpgd/adapter-verse8": "0.6.1",
|
|
76
|
+
"@mpgd/analytics": "0.4.8",
|
|
77
|
+
"@mpgd/capacitor-play-billing": "0.2.3",
|
|
78
|
+
"@mpgd/capacitor-storekit": "0.2.3",
|
|
79
|
+
"@mpgd/game-services": "0.23.0",
|
|
80
|
+
"@mpgd/bridge": "0.12.3",
|
|
81
|
+
"@mpgd/game-runtime": "0.6.6",
|
|
82
|
+
"@mpgd/i18n": "0.6.13",
|
|
83
|
+
"@mpgd/platform": "0.22.0"
|
|
82
84
|
},
|
|
83
85
|
"main": "./dist/index.js",
|
|
84
86
|
"types": "./dist/index.d.ts",
|
|
@@ -11,6 +11,7 @@ installAitHostBridge({
|
|
|
11
11
|
appName: runtimeConfig.appName,
|
|
12
12
|
adGroupIds: runtimeConfig.adGroupIds,
|
|
13
13
|
adPlacementTypes: runtimeConfig.adPlacementTypes,
|
|
14
|
+
...iapBridgeOptions(),
|
|
14
15
|
...identityBridgeOptions(),
|
|
15
16
|
});
|
|
16
17
|
|
|
@@ -21,6 +22,22 @@ if (app !== null) {
|
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The IAP products come from the catalog (`platformProductIds.ait`) at build
|
|
27
|
+
* time, so the wrapper and the game agree on every SKU. The host bridge still
|
|
28
|
+
* sells nothing until the game supplies its server hooks (`prepareIap`,
|
|
29
|
+
* `verifyIapProductGrant`, `readIapEntitlements`, see the mpgd-kit
|
|
30
|
+
* APPS_IN_TOSS_PRODUCTION_EVIDENCE guide): without them every purchase and
|
|
31
|
+
* every `commerce.v2.*` method fails closed, and the grant callback only ever
|
|
32
|
+
* confirms through the game backend.
|
|
33
|
+
*/
|
|
34
|
+
function iapBridgeOptions(): Pick<
|
|
35
|
+
InstallAitHostBridgeOptions,
|
|
36
|
+
'iapProducts' | 'prepareIap' | 'verifyIapProductGrant' | 'readIapEntitlements'
|
|
37
|
+
> {
|
|
38
|
+
return runtimeConfig.iapProducts.length === 0 ? {} : { iapProducts: runtimeConfig.iapProducts };
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
function identityBridgeOptions(): Pick<InstallAitHostBridgeOptions, 'dependencies'> {
|
|
25
42
|
return import.meta.env.VITE_MPGD_AIT_MOCK_IDENTITY === '1'
|
|
26
43
|
? {
|
|
@@ -6,6 +6,8 @@ declare module 'virtual:mpgd-ait-runtime-config' {
|
|
|
6
6
|
string,
|
|
7
7
|
import('@mpgd/adapter-ait/ad-config').AitAdPlacementType
|
|
8
8
|
>>;
|
|
9
|
+
/** Catalog products sold on Apps in Toss (`platformProductIds.ait`), read at build time. */
|
|
10
|
+
readonly iapProducts: readonly import('@mpgd/adapter-ait').AitIapProductConfig[];
|
|
9
11
|
}>;
|
|
10
12
|
|
|
11
13
|
export default runtimeConfig;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
+
import type { AitIapProductConfig } from '@mpgd/adapter-ait';
|
|
3
4
|
import { extractAitAdBridgeConfig, type AitAdBridgeConfig } from '@mpgd/adapter-ait/ad-config';
|
|
5
|
+
import { extractAitIapProductConfig } from '@mpgd/adapter-ait/iap-config';
|
|
4
6
|
import ttsc from '@ttsc/unplugin/vite';
|
|
5
7
|
import { defineConfig, type Plugin } from 'vite';
|
|
6
8
|
|
|
7
9
|
const aitAppName = process.env.MPGD_AIT_APP_NAME?.trim() || '__GAME_NAME__';
|
|
8
10
|
const aitAdConfig = readAitAdConfig(process.env.MPGD_AD_PLACEMENTS_FILE);
|
|
11
|
+
const aitIapProducts = readAitIapProducts(process.env.MPGD_PRODUCT_CATALOG_FILE);
|
|
9
12
|
|
|
10
13
|
export default defineConfig(({ command, isPreview }) => {
|
|
11
14
|
const enableLocalAitMock = command === 'serve'
|
|
@@ -29,6 +32,7 @@ export default defineConfig(({ command, isPreview }) => {
|
|
|
29
32
|
appName: aitAppName,
|
|
30
33
|
adGroupIds: aitAdConfig.adGroupIds,
|
|
31
34
|
adPlacementTypes: aitAdConfig.adPlacementTypes,
|
|
35
|
+
iapProducts: aitIapProducts,
|
|
32
36
|
}),
|
|
33
37
|
...(command === 'build'
|
|
34
38
|
? [ttsc({ project: 'tsconfig.json', plugins: false })]
|
|
@@ -46,6 +50,7 @@ function createAitRuntimeConfigPlugin(input: Readonly<{
|
|
|
46
50
|
readonly appName: string;
|
|
47
51
|
readonly adGroupIds: Readonly<Record<string, string>>;
|
|
48
52
|
readonly adPlacementTypes: AitAdBridgeConfig['adPlacementTypes'];
|
|
53
|
+
readonly iapProducts: readonly AitIapProductConfig[];
|
|
49
54
|
}>): Plugin {
|
|
50
55
|
const publicId = 'virtual:mpgd-ait-runtime-config';
|
|
51
56
|
const resolvedId = `\0${publicId}`;
|
|
@@ -76,3 +81,25 @@ function readAitAdConfig(path: string | undefined): AitAdBridgeConfig {
|
|
|
76
81
|
|
|
77
82
|
return extractAitAdBridgeConfig(parsed, path);
|
|
78
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The Apps in Toss SKUs come from the product catalog (`platformProductIds.ait`)
|
|
87
|
+
* so the wrapper and the game share one source; without a configured catalog
|
|
88
|
+
* file the wrapper sells nothing and the host bridge fails every purchase closed.
|
|
89
|
+
*/
|
|
90
|
+
function readAitIapProducts(path: string | undefined): readonly AitIapProductConfig[] {
|
|
91
|
+
if (path === undefined || path.trim().length === 0) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let parsed: unknown;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(`AIT product catalog file is unreadable or invalid JSON: ${path}`, {
|
|
100
|
+
cause: error,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return extractAitIapProductConfig(parsed, { sourceLabel: path });
|
|
105
|
+
}
|
|
@@ -113,9 +113,16 @@ deployed-app log inspection. See the official
|
|
|
113
113
|
register it once in the developer's global Codex configuration:
|
|
114
114
|
|
|
115
115
|
```sh
|
|
116
|
-
codex mcp add devvit -- npx -y @devvit/mcp
|
|
116
|
+
codex mcp add devvit -- mise --no-config exec node@22 -- npx -y @devvit/mcp@0.0.27
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
The server needs Node 22: `@devvit/mcp` 0.0.27 depends on `better-sqlite3`
|
|
120
|
+
11.9.1, which ships prebuilt binaries for Node 22 but not 24 and does not
|
|
121
|
+
build on 26. `--no-config` keeps mise from passing your mise-managed
|
|
122
|
+
environment to the server. Without mise, register `npx -y @devvit/mcp@0.0.27`
|
|
123
|
+
only if the MCP client's `PATH` resolves `npx` from Node 22. Running
|
|
124
|
+
`codex mcp add devvit` again replaces an older registration.
|
|
125
|
+
|
|
119
126
|
Restart Codex or begin a new task after registering it. Use `devvit_search`
|
|
120
127
|
before broad Devvit documentation exploration. Use the experimental
|
|
121
128
|
`devvit_logs` tool for a deployed app and subreddit when diagnosing production
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@devvit/web": "0.14.1",
|
|
23
23
|
"@mpgd/adapter-devvit": "__MPGD_DEPENDENCY_VERSION_ADAPTER_DEVVIT__",
|
|
24
|
-
"@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__"
|
|
24
|
+
"@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__",
|
|
25
|
+
"@mpgd/catalog": "__MPGD_DEPENDENCY_VERSION_CATALOG__"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@devvit/start": "0.14.1",
|
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
|
|
3
|
-
import { context, createServer, getServerPort, reddit, redis } from '@devvit/web/server';
|
|
3
|
+
import { context, createServer, getServerPort, payments, reddit, redis } from '@devvit/web/server';
|
|
4
4
|
import type { UiResponse } from '@devvit/web/shared';
|
|
5
|
+
import {
|
|
6
|
+
createDevvitCommerceBridgeProducts,
|
|
7
|
+
createDevvitCommerceV2BridgeHandler,
|
|
8
|
+
} from '@mpgd/adapter-devvit/server';
|
|
5
9
|
import { createBridgeRpcRouter, defaultBridgeRpcEndpoint } from '@mpgd/bridge/orpc';
|
|
6
10
|
import { createBridgeRpcNodeHandler } from '@mpgd/bridge/orpc/node';
|
|
11
|
+
import type { ProductCatalog } from '@mpgd/catalog';
|
|
12
|
+
import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
7
13
|
|
|
14
|
+
import devvitManifest from '../../devvit.json';
|
|
8
15
|
import { createDevvitBridgeHandler } from './bridge.js';
|
|
9
16
|
|
|
10
17
|
const maxRequestBodySize = 1_048_576;
|
|
11
18
|
const gameName = '__GAME_NAME__';
|
|
12
19
|
const gameTitle = __GAME_TITLE_TS_LITERAL__;
|
|
20
|
+
/**
|
|
21
|
+
* Commerce v2 opt-in: the catalog's Reddit SKUs (`platformProductIds.reddit`),
|
|
22
|
+
* the `payments` plugin (`permissions.payments` in devvit.json), and a
|
|
23
|
+
* fulfillment endpoint (`payments.endpoints.fulfillOrder`, the handler that
|
|
24
|
+
* writes the backend ledger entry) must all be present. Otherwise the bridge
|
|
25
|
+
* keeps every `commerce.v2.*` method failing closed with
|
|
26
|
+
* `COMMERCE_V2_UNAVAILABLE`, so no checkout opens for an order nothing could
|
|
27
|
+
* grant. The handler only lists orders and products; the grant stays with the
|
|
28
|
+
* fulfillment handler's ledger entry.
|
|
29
|
+
*/
|
|
30
|
+
const commerceProducts = createDevvitCommerceBridgeProducts(productCatalogJson as ProductCatalog);
|
|
31
|
+
const fulfillOrderEndpoint = (devvitManifest as {
|
|
32
|
+
readonly payments?: { readonly endpoints?: { readonly fulfillOrder?: unknown } };
|
|
33
|
+
}).payments?.endpoints?.fulfillOrder;
|
|
34
|
+
const commerceEnabled = devvitManifest.permissions.payments === true
|
|
35
|
+
&& typeof fulfillOrderEndpoint === 'string'
|
|
36
|
+
&& fulfillOrderEndpoint.trim().length > 0
|
|
37
|
+
&& commerceProducts.length > 0;
|
|
38
|
+
const commerce = commerceEnabled
|
|
39
|
+
? createDevvitCommerceV2BridgeHandler({ payments, products: commerceProducts })
|
|
40
|
+
: undefined;
|
|
13
41
|
const handleBridgeRequest = createDevvitBridgeHandler({
|
|
14
42
|
redis,
|
|
15
43
|
currentPlayerId,
|
|
16
44
|
currentDisplayName,
|
|
17
45
|
storageKeyNamespace: gameName,
|
|
46
|
+
...(commerce === undefined ? {} : { commerce }),
|
|
18
47
|
});
|
|
19
48
|
const bridgeRpcHandler = createBridgeRpcNodeHandler(createBridgeRpcRouter(handleBridgeRequest), {
|
|
20
49
|
maxBodySize: maxRequestBodySize,
|
|
@@ -122,6 +151,11 @@ server.on('error', (error) => {
|
|
|
122
151
|
|
|
123
152
|
server.listen(port, () => {
|
|
124
153
|
console.log(`devvit server listening on ${port}`);
|
|
154
|
+
console.log(
|
|
155
|
+
commerceEnabled
|
|
156
|
+
? `devvit commerce v2 serving ${commerceProducts.length} catalog product(s) over the payments plugin`
|
|
157
|
+
: 'devvit commerce v2 disabled: needs permissions.payments, payments.endpoints.fulfillOrder, and reddit SKUs in the catalog',
|
|
158
|
+
);
|
|
125
159
|
});
|
|
126
160
|
|
|
127
161
|
function errorMessage(error: unknown): string {
|
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
"@mpgd/adapter-verse8": "__MPGD_DEPENDENCY_VERSION_ADAPTER_VERSE8__",
|
|
67
67
|
"@mpgd/analytics": "__MPGD_DEPENDENCY_VERSION_ANALYTICS__",
|
|
68
68
|
"@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__",
|
|
69
|
+
"@mpgd/capacitor-play-billing": "__MPGD_DEPENDENCY_VERSION_CAPACITOR_PLAY_BILLING__",
|
|
70
|
+
"@mpgd/capacitor-storekit": "__MPGD_DEPENDENCY_VERSION_CAPACITOR_STOREKIT__",
|
|
69
71
|
"@mpgd/catalog": "__MPGD_DEPENDENCY_VERSION_CATALOG__",
|
|
70
72
|
"@mpgd/game-services": "__MPGD_DEPENDENCY_VERSION_GAME_SERVICES__",
|
|
71
73
|
"@mpgd/i18n": "__MPGD_DEPENDENCY_VERSION_I18N__",
|
|
@@ -112,7 +112,14 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
|
|
|
112
112
|
});
|
|
113
113
|
gameRuntime = ownedRuntime;
|
|
114
114
|
const gameServices = ownedRuntime.services;
|
|
115
|
-
|
|
115
|
+
try {
|
|
116
|
+
await ownedRuntime.reconcile();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// A journal the platform storage cannot read must not stop the game:
|
|
119
|
+
// recovery runs again on resume, and purchases and rewarded ads fail
|
|
120
|
+
// closed until the journal is readable.
|
|
121
|
+
console.error('[game-services] startup recovery failed', error);
|
|
122
|
+
}
|
|
116
123
|
|
|
117
124
|
await analytics.track({
|
|
118
125
|
name: 'game_started',
|
|
@@ -3,6 +3,15 @@ import type { PlatformGateway } from '@mpgd/platform';
|
|
|
3
3
|
|
|
4
4
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Apps in Toss gateway over the wrapper's host bridge. Commerce lives on the
|
|
8
|
+
* host side: the wrapper (`apps/target-ait`) installs the IAP products from
|
|
9
|
+
* the catalog's `platformProductIds.ait` entries and serves the six
|
|
10
|
+
* `commerce.v2.*` methods plus the `commerceV2` event channel, so the gateway
|
|
11
|
+
* exposes `commerceV2` whenever the installed bridge has that channel. The
|
|
12
|
+
* host's grant callback only confirms through the game backend; nothing is
|
|
13
|
+
* granted from this side.
|
|
14
|
+
*/
|
|
6
15
|
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
7
16
|
return createAitPlatformGateway({
|
|
8
17
|
appVersion: runtime.appVersion,
|
|
@@ -3,6 +3,13 @@ import type { PlatformGateway } from '@mpgd/platform';
|
|
|
3
3
|
|
|
4
4
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Local Apps in Toss playtest. The sandbox bridge keeps its build-id gate: only
|
|
8
|
+
* the `ait-sandbox` build (`pnpm dev:ait`) gets the `commerceV2` channel and
|
|
9
|
+
* the evidence-only v2 stub; any other build id gets no channel, so the
|
|
10
|
+
* gateway offers no `commerceV2` and the v2 methods answer
|
|
11
|
+
* `COMMERCE_V2_UNAVAILABLE`.
|
|
12
|
+
*/
|
|
6
13
|
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
7
14
|
return createAitPlatformGateway({
|
|
8
15
|
appVersion: runtime.appVersion,
|
|
@@ -1,12 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createCapacitorPlatformGateway,
|
|
3
|
+
type CapacitorServiceProvider,
|
|
4
|
+
} from '@mpgd/adapter-capacitor';
|
|
5
|
+
import {
|
|
6
|
+
createCapacitorPlayBillingServiceProvider,
|
|
7
|
+
type PlayBillingCommerceProductMapping,
|
|
8
|
+
} from '@mpgd/capacitor-play-billing';
|
|
9
|
+
import type { ProductCatalog } from '@mpgd/catalog';
|
|
10
|
+
import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
2
11
|
import type { PlatformGateway } from '@mpgd/platform';
|
|
3
12
|
|
|
13
|
+
import { resolvePlayBillingAccountBinding } from '../commerceAccountBinding';
|
|
14
|
+
import { resolveStarterStoreProducts } from '../commerceProducts';
|
|
4
15
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
5
16
|
|
|
17
|
+
const productCatalog = productCatalogJson as ProductCatalog;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Android gateway with the Google Play Billing commerce v2 service provider
|
|
21
|
+
* (`@mpgd/capacitor-play-billing`) registered for the catalog products that
|
|
22
|
+
* name a Play SKU. The provider serves the `commerce.v2.*` and v1 `commerce.*`
|
|
23
|
+
* bridge methods and never grants: the game-services reconciler verifies each
|
|
24
|
+
* purchase through the backend ledger. A shell built without the native
|
|
25
|
+
* plugin (`mpgd target init capacitor --providers play-billing` installs it)
|
|
26
|
+
* still runs; the store then answers `configuration-required`.
|
|
27
|
+
*/
|
|
6
28
|
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
7
|
-
|
|
29
|
+
let gateway: PlatformGateway | undefined;
|
|
30
|
+
const providers = createStoreProviders(runtime, () => gateway);
|
|
31
|
+
gateway = createCapacitorPlatformGateway({
|
|
8
32
|
target: 'android',
|
|
9
33
|
appVersion: runtime.appVersion,
|
|
10
34
|
buildId: runtime.buildId,
|
|
35
|
+
providers,
|
|
11
36
|
});
|
|
37
|
+
return gateway;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createStoreProviders(
|
|
41
|
+
runtime: RuntimeConfig,
|
|
42
|
+
resolveGateway: () => PlatformGateway | undefined,
|
|
43
|
+
): readonly CapacitorServiceProvider[] {
|
|
44
|
+
// Play sells one-time products only (RFC D-02); a subscription stays off the store.
|
|
45
|
+
const products = resolveStarterStoreProducts(productCatalog, runtime.configTarget)
|
|
46
|
+
.flatMap((product): PlayBillingCommerceProductMapping[] =>
|
|
47
|
+
product.kind === 'subscription' ? [] : [{ ...product, kind: product.kind }]);
|
|
48
|
+
if (products.length === 0) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
createCapacitorPlayBillingServiceProvider({
|
|
53
|
+
products,
|
|
54
|
+
// The backend's resolveObfuscatedAccountId must derive the same value; see commerceAccountBinding.ts.
|
|
55
|
+
accountBinding: async () => {
|
|
56
|
+
const gateway = resolveGateway();
|
|
57
|
+
return gateway === undefined ? undefined : resolvePlayBillingAccountBinding(gateway);
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
12
61
|
}
|
|
@@ -1,12 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createCapacitorPlatformGateway,
|
|
3
|
+
type CapacitorServiceProvider,
|
|
4
|
+
} from '@mpgd/adapter-capacitor';
|
|
5
|
+
import { createCapacitorStoreKitServiceProvider } from '@mpgd/capacitor-storekit';
|
|
6
|
+
import type { ProductCatalog } from '@mpgd/catalog';
|
|
7
|
+
import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
2
8
|
import type { PlatformGateway } from '@mpgd/platform';
|
|
3
9
|
|
|
10
|
+
import { resolveStoreKitAppAccountToken } from '../commerceAccountBinding';
|
|
11
|
+
import { resolveStarterStoreProducts } from '../commerceProducts';
|
|
4
12
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
5
13
|
|
|
14
|
+
const productCatalog = productCatalogJson as ProductCatalog;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* iOS gateway with the StoreKit commerce v2 service provider
|
|
18
|
+
* (`@mpgd/capacitor-storekit`) registered for the catalog products that name
|
|
19
|
+
* an App Store product id. The provider serves the `commerce.v2.*` and v1
|
|
20
|
+
* `commerce.*` bridge methods and never grants or finishes on its own: the
|
|
21
|
+
* game-services reconciler verifies the signed transaction through the
|
|
22
|
+
* backend ledger and only then finishes it. A shell built without the native
|
|
23
|
+
* plugin (`mpgd target init capacitor --providers storekit` installs it)
|
|
24
|
+
* still runs; the store then answers `configuration-required`.
|
|
25
|
+
*/
|
|
6
26
|
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
7
|
-
|
|
27
|
+
let gateway: PlatformGateway | undefined;
|
|
28
|
+
const providers = createStoreProviders(runtime, () => gateway);
|
|
29
|
+
gateway = createCapacitorPlatformGateway({
|
|
8
30
|
target: 'ios',
|
|
9
31
|
appVersion: runtime.appVersion,
|
|
10
32
|
buildId: runtime.buildId,
|
|
33
|
+
providers,
|
|
11
34
|
});
|
|
35
|
+
return gateway;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createStoreProviders(
|
|
39
|
+
runtime: RuntimeConfig,
|
|
40
|
+
resolveGateway: () => PlatformGateway | undefined,
|
|
41
|
+
): readonly CapacitorServiceProvider[] {
|
|
42
|
+
// StoreKit sells one-time products only (RFC D-02); a subscription stays off the store.
|
|
43
|
+
const products = resolveStarterStoreProducts(productCatalog, runtime.configTarget)
|
|
44
|
+
.filter((product) => product.kind !== 'subscription');
|
|
45
|
+
if (products.length === 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
createCapacitorStoreKitServiceProvider({
|
|
50
|
+
products,
|
|
51
|
+
// The backend's resolveAppAccountToken must derive the same value; see commerceAccountBinding.ts.
|
|
52
|
+
getAppAccountToken: async () => {
|
|
53
|
+
const gateway = resolveGateway();
|
|
54
|
+
if (gateway === undefined) {
|
|
55
|
+
throw new Error('The iOS gateway is not ready for a StoreKit purchase.');
|
|
56
|
+
}
|
|
57
|
+
return resolveStoreKitAppAccountToken(gateway);
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
12
61
|
}
|
|
@@ -1,37 +1,73 @@
|
|
|
1
1
|
import { createBrowserPlatformGateway } from '@mpgd/adapter-browser';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
createMicrosoftStoreCommerceProvider,
|
|
4
|
+
withMicrosoftStoreCommerceProvider,
|
|
5
|
+
type MicrosoftStoreCommerceProduct,
|
|
5
6
|
} from '@mpgd/adapter-browser/microsoft-store';
|
|
7
|
+
import { resolveProductPlatformId, type ProductCatalog } from '@mpgd/catalog';
|
|
8
|
+
import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
6
9
|
import type { PlatformGateway } from '@mpgd/platform';
|
|
7
10
|
|
|
8
11
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
9
12
|
|
|
13
|
+
const productCatalog = productCatalogJson as ProductCatalog;
|
|
10
14
|
const browser = createBrowserPlatformGateway();
|
|
11
15
|
const base: PlatformGateway = { ...browser, target: 'microsoft-store' };
|
|
12
|
-
const commerce = createMicrosoftStoreCommerceAdapter({
|
|
13
|
-
getRecoveryScope: () => 'configuration-required',
|
|
14
|
-
products: [],
|
|
15
|
-
authority: {
|
|
16
|
-
async getAvailability() {
|
|
17
|
-
return 'configuration-required';
|
|
18
|
-
},
|
|
19
|
-
async claimRecoveryOwnership() {
|
|
20
|
-
return { status: 'denied' } as const;
|
|
21
|
-
},
|
|
22
|
-
async hasRecoveryOwnership() {
|
|
23
|
-
return { status: 'denied' } as const;
|
|
24
|
-
},
|
|
25
|
-
async verifyAndGrant() {
|
|
26
|
-
return { status: 'failed' };
|
|
27
|
-
},
|
|
28
|
-
async getEntitlements() {
|
|
29
|
-
return [];
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
16
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Microsoft Store products from the catalog: the Partner Center add-on
|
|
19
|
+
* product id is `platformProductIds['microsoft-store']`, and the Digital
|
|
20
|
+
* Goods API currently sells consumables only. Title, description, and price
|
|
21
|
+
* are replaced by the Store's own details when the catalog loads.
|
|
22
|
+
*/
|
|
23
|
+
function resolveMicrosoftStoreProducts(target: string): readonly MicrosoftStoreCommerceProduct[] {
|
|
24
|
+
return productCatalog.products.flatMap((product) => {
|
|
25
|
+
const inAppOfferToken = resolveProductPlatformId(product, target);
|
|
26
|
+
return inAppOfferToken === undefined || product.type !== 'consumable'
|
|
27
|
+
? []
|
|
28
|
+
: [
|
|
29
|
+
{
|
|
30
|
+
info: {
|
|
31
|
+
id: product.id,
|
|
32
|
+
type: product.type,
|
|
33
|
+
title: product.id,
|
|
34
|
+
description: '',
|
|
35
|
+
price: { formatted: '', currencyCode: 'USD' },
|
|
36
|
+
},
|
|
37
|
+
inAppOfferToken,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Commerce v2 over the Digital Goods path (`createMicrosoftStoreCommerceProvider`)
|
|
45
|
+
* installed as `commerceV2` with the v1 wrapper. The purchase authority stays
|
|
46
|
+
* `configuration-required` until the game points it at its backend: the
|
|
47
|
+
* authority's ledger entry is the only settlement, and the reconciler still
|
|
48
|
+
* verifies it through game-services before anything is granted.
|
|
49
|
+
*/
|
|
50
|
+
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
51
|
+
const commerce = createMicrosoftStoreCommerceProvider({
|
|
52
|
+
getRecoveryScope: () => 'configuration-required',
|
|
53
|
+
products: resolveMicrosoftStoreProducts(runtime.configTarget),
|
|
54
|
+
authority: {
|
|
55
|
+
async getAvailability() {
|
|
56
|
+
return 'configuration-required';
|
|
57
|
+
},
|
|
58
|
+
async claimRecoveryOwnership() {
|
|
59
|
+
return { status: 'denied' } as const;
|
|
60
|
+
},
|
|
61
|
+
async hasRecoveryOwnership() {
|
|
62
|
+
return { status: 'denied' } as const;
|
|
63
|
+
},
|
|
64
|
+
async verifyAndGrant() {
|
|
65
|
+
return { status: 'failed' };
|
|
66
|
+
},
|
|
67
|
+
async getEntitlements() {
|
|
68
|
+
return [];
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
return withMicrosoftStoreCommerceProvider(base, commerce);
|
|
37
73
|
}
|
|
@@ -1,11 +1,46 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createDevvitCommerceProducts,
|
|
3
|
+
createDevvitPlatformGateway,
|
|
4
|
+
type DevvitGatewayCommerceOptions,
|
|
5
|
+
} from '@mpgd/adapter-devvit';
|
|
6
|
+
import { createDevvitWebPaymentsClient } from '@mpgd/adapter-devvit/web';
|
|
7
|
+
import type { ProductCatalog } from '@mpgd/catalog';
|
|
8
|
+
import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
2
9
|
import type { PlatformGateway } from '@mpgd/platform';
|
|
3
10
|
|
|
4
11
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
5
12
|
|
|
13
|
+
const productCatalog = productCatalogJson as ProductCatalog;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reddit gateway with the Devvit commerce v2 provider installed when the
|
|
17
|
+
* catalog names Reddit SKUs (`platformProductIds.reddit`, the same key the
|
|
18
|
+
* Devvit server reads, whatever the deployment target is called). The webview
|
|
19
|
+
* only opens the checkout; the paid-order list comes from the server bridge,
|
|
20
|
+
* which serves it only after the Devvit server opted in
|
|
21
|
+
* (`permissions.payments` plus the same catalog SKUs), and the grant comes
|
|
22
|
+
* from the fulfillment handler's ledger entry. Without SKUs the gateway
|
|
23
|
+
* offers no `commerceV2` at all.
|
|
24
|
+
*/
|
|
6
25
|
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
26
|
+
const commerce = createCommerceOptions();
|
|
7
27
|
return createDevvitPlatformGateway({
|
|
8
28
|
appVersion: runtime.appVersion,
|
|
9
29
|
buildId: runtime.buildId,
|
|
30
|
+
...(commerce === undefined ? {} : { commerce }),
|
|
10
31
|
});
|
|
11
32
|
}
|
|
33
|
+
|
|
34
|
+
function createCommerceOptions(): DevvitGatewayCommerceOptions | undefined {
|
|
35
|
+
const products = createDevvitCommerceProducts(productCatalog);
|
|
36
|
+
if (products.length === 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
products,
|
|
41
|
+
client: createDevvitWebPaymentsClient(),
|
|
42
|
+
onCheckoutError: (error) => {
|
|
43
|
+
console.warn('[devvit] checkout answered without a usable result.', error);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createVerse8CommerceProducts,
|
|
3
|
+
createVerse8PlatformGateway,
|
|
4
|
+
type Verse8CommerceOptions,
|
|
5
|
+
} from '@mpgd/adapter-verse8';
|
|
6
|
+
import {
|
|
7
|
+
resolveAdPlacementPlatformId,
|
|
8
|
+
type AdPlacements,
|
|
9
|
+
type ProductCatalog,
|
|
10
|
+
} from '@mpgd/catalog';
|
|
11
|
+
import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
3
12
|
import adPlacementsJson from '@mpgd/catalog/placements.json';
|
|
4
13
|
import type { PlatformGateway } from '@mpgd/platform';
|
|
5
14
|
|
|
6
15
|
import type { RuntimeConfig } from '../runtimeDetector';
|
|
16
|
+
import { resolveStarterVerse8Agent8Client } from '../verse8Agent8Client';
|
|
7
17
|
|
|
18
|
+
const productCatalog = productCatalogJson as ProductCatalog;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verse8 gateway. VXShop and the commerce v2 provider are installed only when
|
|
22
|
+
* the game provides its authenticated Agent8 client (`verse8Agent8Client.ts`)
|
|
23
|
+
* and the catalog names VXShop product ids (`platformProductIds.verse8`):
|
|
24
|
+
* `vxShop.purchases` reads the purchases the Agent8 server recorded, which is
|
|
25
|
+
* the server-push lookup the reconciler verifies through the backend ledger.
|
|
26
|
+
* The VXShop dialog callback never grants.
|
|
27
|
+
*/
|
|
8
28
|
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
9
29
|
const adPlacements = adPlacementsJson as AdPlacements;
|
|
10
30
|
const placementIds = new Map(
|
|
@@ -19,10 +39,28 @@ export async function createBuildGateway(runtime: RuntimeConfig): Promise<Platfo
|
|
|
19
39
|
: [[placement.id, platformPlacementId] as const];
|
|
20
40
|
}),
|
|
21
41
|
);
|
|
42
|
+
const vxShop = createVxShopOptions(runtime);
|
|
22
43
|
|
|
23
44
|
return createVerse8PlatformGateway({
|
|
24
45
|
resolveAdPlacementId(placementId) {
|
|
25
46
|
return placementIds.get(placementId);
|
|
26
47
|
},
|
|
48
|
+
...(vxShop === undefined ? {} : { vxShop }),
|
|
27
49
|
});
|
|
28
50
|
}
|
|
51
|
+
|
|
52
|
+
function createVxShopOptions(runtime: RuntimeConfig): Verse8CommerceOptions | undefined {
|
|
53
|
+
const agent8 = resolveStarterVerse8Agent8Client();
|
|
54
|
+
const products = createVerse8CommerceProducts(productCatalog, runtime.configTarget);
|
|
55
|
+
if (agent8 === undefined || products.length === 0) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
purchaseEventAuthority: 'agent8-server',
|
|
60
|
+
products,
|
|
61
|
+
loadEntitlements: async () => (await agent8.getCommerceSnapshot()).entitlements,
|
|
62
|
+
purchases: {
|
|
63
|
+
listPurchases: () => agent8.listPurchases(),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|