@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.
Files changed (26) hide show
  1. package/dist/capacitor-shell-starter.d.ts +57 -1
  2. package/dist/capacitor-shell-starter.js +311 -34
  3. package/dist/index.js +23 -5
  4. package/dist/native-build-info.json +2 -2
  5. package/dist/native-build-target.js +613 -316
  6. package/package.json +17 -15
  7. package/templates/phaser-game/apps/target-ait/src/main.ts +17 -0
  8. package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +2 -0
  9. package/templates/phaser-game/apps/target-ait/vite.config.ts +27 -0
  10. package/templates/phaser-game/apps/target-devvit/README.md +8 -1
  11. package/templates/phaser-game/apps/target-devvit/package.json +2 -1
  12. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +35 -1
  13. package/templates/phaser-game/package.json +2 -0
  14. package/templates/phaser-game/src/main.ts +8 -1
  15. package/templates/phaser-game/src/platform/buildGateways/ait.ts +9 -0
  16. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +7 -0
  17. package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +51 -2
  18. package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +51 -2
  19. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +62 -26
  20. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +36 -1
  21. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +40 -2
  22. package/templates/phaser-game/src/platform/commerceAccountBinding.ts +79 -0
  23. package/templates/phaser-game/src/platform/commerceProducts.ts +69 -0
  24. package/templates/phaser-game/src/platform/gameServices.ts +22 -2
  25. package/templates/phaser-game/src/platform/monetizationJournal.ts +286 -0
  26. 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.45.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/target-config": "0.22.0",
62
- "@mpgd/catalog": "0.9.2",
63
- "@mpgd/phaser-assets": "0.9.0"
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-ait": "0.15.1",
72
- "@mpgd/adapter-browser": "0.10.1",
73
- "@mpgd/adapter-capacitor": "0.9.1",
74
- "@mpgd/adapter-devvit": "0.10.0",
75
- "@mpgd/bridge": "0.12.2",
76
- "@mpgd/analytics": "0.4.7",
77
- "@mpgd/adapter-verse8": "0.5.0",
78
- "@mpgd/game-services": "0.21.1",
79
- "@mpgd/i18n": "0.6.12",
80
- "@mpgd/game-runtime": "0.6.4",
81
- "@mpgd/platform": "0.21.0"
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
- await ownedRuntime.reconcile();
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 { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor';
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
- return createCapacitorPlatformGateway({
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 { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor';
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
- return createCapacitorPlatformGateway({
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
- createMicrosoftStoreCommerceAdapter,
4
- withMicrosoftStoreCommerceAdapter,
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
- /** Configure Store products, authenticated recovery scope, and authority before checkout. */
35
- export async function createBuildGateway(_runtime: RuntimeConfig): Promise<PlatformGateway> {
36
- return withMicrosoftStoreCommerceAdapter(base, commerce);
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 { createDevvitPlatformGateway } from '@mpgd/adapter-devvit';
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 { createVerse8PlatformGateway } from '@mpgd/adapter-verse8';
2
- import { resolveAdPlacementPlatformId, type AdPlacements } from '@mpgd/catalog';
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
+ }