@mpgd/cli 0.46.1 → 0.48.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 (62) hide show
  1. package/dist/capacitor-shell-starter.d.ts +57 -1
  2. package/dist/capacitor-shell-starter.js +311 -34
  3. package/dist/command-launcher.d.ts +13 -0
  4. package/dist/command-launcher.js +24 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +149 -12
  7. package/dist/microsoft-store-submission.js +11 -6
  8. package/dist/native-build-info.json +2 -2
  9. package/dist/native-build-target.js +2413 -345
  10. package/dist/windows-host/Core/BridgeCommand.cs +17 -0
  11. package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
  12. package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
  13. package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
  14. package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
  15. package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
  16. package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
  17. package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
  18. package/dist/windows-host/README.md +118 -0
  19. package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
  20. package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
  21. package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
  22. package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
  23. package/dist/windows-native-tools.d.ts +4 -0
  24. package/dist/windows-native-tools.js +21 -0
  25. package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
  26. package/package.json +24 -14
  27. package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
  28. package/templates/lite-overlay/AGENTS.md +27 -0
  29. package/templates/lite-overlay/README.md +37 -0
  30. package/templates/lite-overlay/agent/acceptance.md +27 -0
  31. package/templates/lite-overlay/agent/brief.md +17 -0
  32. package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
  33. package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
  34. package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
  35. package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
  36. package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
  37. package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
  38. package/templates/phaser-game/README.md +5 -3
  39. package/templates/phaser-game/apps/target-ait/package.json +2 -2
  40. package/templates/phaser-game/apps/target-ait/src/main.ts +17 -0
  41. package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +2 -0
  42. package/templates/phaser-game/apps/target-ait/vite.config.ts +27 -0
  43. package/templates/phaser-game/apps/target-devvit/package.json +2 -1
  44. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
  45. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +35 -1
  46. package/templates/phaser-game/package.json +3 -1
  47. package/templates/phaser-game/pnpm-workspace.yaml +2 -0
  48. package/templates/phaser-game/src/env.d.ts +2 -0
  49. package/templates/phaser-game/src/main.ts +8 -1
  50. package/templates/phaser-game/src/platform/buildGateways/ait.ts +9 -0
  51. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +13 -1
  52. package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +51 -2
  53. package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +51 -2
  54. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +59 -26
  55. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +36 -1
  56. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +39 -2
  57. package/templates/phaser-game/src/platform/commerceAccountBinding.ts +79 -0
  58. package/templates/phaser-game/src/platform/commerceProducts.ts +69 -0
  59. package/templates/phaser-game/src/platform/gameServices.ts +22 -2
  60. package/templates/phaser-game/src/platform/monetizationJournal.ts +286 -0
  61. package/templates/phaser-game/src/platform/verse8Agent8Client.ts +22 -0
  62. package/templates/phaser-game/vite.shared.ts +40 -1
@@ -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
+ }
@@ -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,7 +1,10 @@
1
1
  import {
2
+ bridgeEventProtocol,
2
3
  bridgeStorageLoadProtocol,
3
4
  createBridgeError,
4
5
  createCommerceV2UnavailableBridgeError,
6
+ createNotificationsV2UnavailableBridgeError,
7
+ isBridgeNotificationsV2Method,
5
8
  type BridgeRequest,
6
9
  type BridgeResponse,
7
10
  type BridgeStorageLoadData,
@@ -90,10 +93,15 @@ export function createDevvitBridgeHandler(
90
93
  }
91
94
 
92
95
  return async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse> {
96
+ // Notifications v2 arrives with the Devvit provider (P3), which serves the
97
+ // game-rendered opt-in from this bridge; until then every v2 method
98
+ // answers the dedicated code, and the v1 `notifications.*` answers stay.
99
+ if (isBridgeNotificationsV2Method(input.method)) {
100
+ return createNotificationsV2UnavailableBridgeError(input, 'Devvit server bridge');
101
+ }
93
102
  switch (input.method) {
94
103
  case 'runtime.getCapabilities':
95
104
  return ok(input, {
96
- nativeIap: false,
97
105
  nativeAds: false,
98
106
  rewardedAds: false,
99
107
  interstitialAds: false,
@@ -104,6 +112,7 @@ export function createDevvitBridgeHandler(
104
112
  socialShare: false,
105
113
  haptics: false,
106
114
  localizedContent: true,
115
+ commerce: await readCommerceCapabilities(input, dependencies.commerce),
107
116
  });
108
117
 
109
118
  case 'identity.getPlayer': {
@@ -164,21 +173,6 @@ export function createDevvitBridgeHandler(
164
173
  case 'notifications.requestSubscription':
165
174
  return ok(input, 'unavailable');
166
175
 
167
- case 'commerce.getProducts':
168
- case 'commerce.getEntitlements':
169
- return ok(input, []);
170
-
171
- case 'commerce.purchase':
172
- return ok(input, {
173
- status: 'cancelled',
174
- entitlementIds: [],
175
- });
176
-
177
- case 'commerce.restore':
178
- return ok(input, {
179
- restoredEntitlements: [],
180
- });
181
-
182
176
  case 'ads.preload':
183
177
  return ok(input, {});
184
178
 
@@ -211,8 +205,9 @@ export function createDevvitBridgeHandler(
211
205
  case 'storage.save':
212
206
  return saveStorage(input, dependencies, storageKeyNamespace, warn);
213
207
 
214
- // Commerce v2 (B6): served by the configured handler over the payments
215
- // plugin; the checkout itself opens in the webview.
208
+ // Commerce v2: served by the configured handler over the payments
209
+ // plugin; the checkout itself opens in the webview. The removed v1
210
+ // `commerce.*` methods fall to UNSUPPORTED_METHOD below.
216
211
  case 'commerce.v2.getCapabilities':
217
212
  case 'commerce.v2.fetchProducts':
218
213
  case 'commerce.v2.requestPurchase':
@@ -233,6 +228,130 @@ export function createDevvitBridgeHandler(
233
228
  };
234
229
  }
235
230
 
231
+ /**
232
+ * The all-false commerce capability block of a server that sells nothing
233
+ * (`createUnsupportedCommerceCapabilities()` of `@mpgd/platform`, kept equal by
234
+ * the bridge test). A fresh object on every read.
235
+ */
236
+ function createUnsupportedCommerceBlock() {
237
+ return {
238
+ purchase: { consumable: false, durable: false, subscription: false, quantity: false },
239
+ catalog: { products: false, offers: false, localizedPrice: false },
240
+ lifecycle: {
241
+ availablePurchases: false,
242
+ pending: false,
243
+ restore: false,
244
+ events: false,
245
+ finish: 'none',
246
+ },
247
+ authority: { evidence: 'client-token', accountBinding: 'optional' },
248
+ availability: 'unsupported',
249
+ };
250
+ }
251
+
252
+ const commerceBlockShape = {
253
+ purchase: {
254
+ consumable: 'boolean',
255
+ durable: 'boolean',
256
+ subscription: 'boolean',
257
+ quantity: 'boolean',
258
+ },
259
+ catalog: { products: 'boolean', offers: 'boolean', localizedPrice: 'boolean' },
260
+ lifecycle: {
261
+ availablePurchases: 'boolean',
262
+ pending: 'boolean',
263
+ restore: 'boolean',
264
+ events: 'boolean',
265
+ finish: ['client', 'server', 'host', 'none'],
266
+ },
267
+ authority: {
268
+ evidence: ['client-token', 'host-callback', 'adapter-settled', 'server-push'],
269
+ accountBinding: ['required', 'optional', 'host'],
270
+ },
271
+ } as const;
272
+ const commerceAvailabilities: readonly unknown[] = [
273
+ 'available',
274
+ 'unsupported',
275
+ 'configuration-required',
276
+ 'action-required',
277
+ 'temporarily-unavailable',
278
+ ];
279
+
280
+ function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
281
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
282
+ }
283
+
284
+ function hasExactKeys(value: Readonly<Record<string, unknown>>, keys: readonly string[]): boolean {
285
+ const own = Object.keys(value);
286
+ return own.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
287
+ }
288
+
289
+ /**
290
+ * A fresh copy of a well-formed commerce capability block (the exact keys and
291
+ * values `PlatformCapabilities.commerce` allows), or undefined for anything
292
+ * else, so a malformed handler answer never reaches the snapshot.
293
+ */
294
+ function copyCommerceBlock(value: unknown): Record<string, unknown> | undefined {
295
+ if (!isPlainRecord(value)
296
+ || !hasExactKeys(value, [...Object.keys(commerceBlockShape), 'availability'])
297
+ || !commerceAvailabilities.includes(value.availability)) {
298
+ return undefined;
299
+ }
300
+ const copy: Record<string, unknown> = { availability: value.availability };
301
+ for (const [section, fields] of Object.entries(commerceBlockShape)) {
302
+ const source = value[section];
303
+ if (!isPlainRecord(source) || !hasExactKeys(source, Object.keys(fields))) {
304
+ return undefined;
305
+ }
306
+ const sectionCopy: Record<string, unknown> = {};
307
+ for (const [field, rule] of Object.entries(fields)) {
308
+ const fieldValue = source[field];
309
+ const valid = rule === 'boolean'
310
+ ? typeof fieldValue === 'boolean'
311
+ : (rule as readonly unknown[]).includes(fieldValue);
312
+ if (!valid) {
313
+ return undefined;
314
+ }
315
+ sectionCopy[field] = fieldValue;
316
+ }
317
+ copy[section] = sectionCopy;
318
+ }
319
+ return copy;
320
+ }
321
+
322
+ /**
323
+ * The `commerce` block of the server snapshot: what the configured commerce
324
+ * handler reports for this server, otherwise the unsupported block. It fails
325
+ * closed: a missing handler, a handler that throws or answers an error, or an
326
+ * answer without a well-formed capability block sells nothing. The webview
327
+ * gateway owns the block it finally reports (its provider's, or the
328
+ * unsupported one).
329
+ */
330
+ async function readCommerceCapabilities(
331
+ input: BridgeRequest,
332
+ handler: DevvitBridgeCommerceHandler | undefined,
333
+ ): Promise<unknown> {
334
+ if (handler === undefined) {
335
+ return createUnsupportedCommerceBlock();
336
+ }
337
+
338
+ try {
339
+ const response = await handler.request({
340
+ ...input,
341
+ method: 'commerce.v2.getCapabilities',
342
+ payload: { protocol: bridgeEventProtocol },
343
+ });
344
+ const data: unknown = response.ok ? response.data : undefined;
345
+ const capabilities = typeof data === 'object' && data !== null
346
+ ? (data as { readonly capabilities?: unknown }).capabilities
347
+ : undefined;
348
+
349
+ return copyCommerceBlock(capabilities) ?? createUnsupportedCommerceBlock();
350
+ } catch {
351
+ return createUnsupportedCommerceBlock();
352
+ }
353
+ }
354
+
236
355
  async function loadStorage(
237
356
  input: BridgeRequest,
238
357
  dependencies: DevvitBridgeHandlerDependencies,
@@ -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 {
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@ait-co/polyfill": "^0.1.19",
59
- "@apps-in-toss/web-framework": "3.5.0",
59
+ "@apps-in-toss/web-framework": "3.6.0",
60
60
  "@capacitor/app": "8.1.1",
61
61
  "@capacitor/core": "8.5.2",
62
62
  "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION_ADAPTER_AIT__",
@@ -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__",
@@ -1,3 +1,5 @@
1
+ # Run the generated POSIX target scripts consistently on Windows and Unix.
2
+ shellEmulator: true
1
3
  packages:
2
4
  - '.'
3
5
  - 'apps/*'
@@ -2,6 +2,8 @@
2
2
 
3
3
  declare const __APP_TARGET__: string;
4
4
  declare const __MPGD_CONFIG_TARGET__: string;
5
+ /** The Apps in Toss app name `intoss://` share links carry; see `vite.shared.ts`. */
6
+ declare const __MPGD_AIT_APP_NAME__: string;
5
7
  declare const __MPGD_PLATFORM_TARGET__:
6
8
  | import('@mpgd/target-config').EffectivePlatformTargetMetadata
7
9
  | undefined;
@@ -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 `commerce` 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,10 +3,22 @@ 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 commerce event channel and
9
+ * the evidence-only v2 stub; any other build id gets no channel, so the
10
+ * gateway offers no `commerce` provider and the v2 methods answer
11
+ * `COMMERCE_V2_UNAVAILABLE`. Every build answers the `notifications.v2.*`
12
+ * methods with `NOTIFICATIONS_V2_UNAVAILABLE` until the Apps in Toss
13
+ * notifications provider lands.
14
+ */
6
15
  export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
7
16
  return createAitPlatformGateway({
8
17
  appVersion: runtime.appVersion,
9
18
  buildId: runtime.buildId,
10
- fallbackBridge: createAitSandboxBridge({ buildId: runtime.buildId }),
19
+ fallbackBridge: createAitSandboxBridge({
20
+ appName: __MPGD_AIT_APP_NAME__,
21
+ buildId: runtime.buildId,
22
+ }),
11
23
  });
12
24
  }
@@ -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,70 @@
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 over the Digital Goods path (`createMicrosoftStoreCommerceProvider`)
45
+ * installed as `gateway.commerce` with its capability block. 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
+ },
68
+ });
69
+ return withMicrosoftStoreCommerceProvider(base, commerce);
37
70
  }