@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.
- package/dist/capacitor-shell-starter.d.ts +57 -1
- package/dist/capacitor-shell-starter.js +311 -34
- package/dist/command-launcher.d.ts +13 -0
- package/dist/command-launcher.js +24 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +149 -12
- package/dist/microsoft-store-submission.js +11 -6
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +2413 -345
- package/dist/windows-host/Core/BridgeCommand.cs +17 -0
- package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
- package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
- package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
- package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
- package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
- package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
- package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
- package/dist/windows-host/README.md +118 -0
- package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
- package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
- package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
- package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
- package/dist/windows-native-tools.d.ts +4 -0
- package/dist/windows-native-tools.js +21 -0
- package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
- package/package.json +24 -14
- package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
- package/templates/lite-overlay/AGENTS.md +27 -0
- package/templates/lite-overlay/README.md +37 -0
- package/templates/lite-overlay/agent/acceptance.md +27 -0
- package/templates/lite-overlay/agent/brief.md +17 -0
- package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
- package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
- package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
- package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
- package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
- package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
- package/templates/phaser-game/README.md +5 -3
- package/templates/phaser-game/apps/target-ait/package.json +2 -2
- 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/package.json +2 -1
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +35 -1
- package/templates/phaser-game/package.json +3 -1
- package/templates/phaser-game/pnpm-workspace.yaml +2 -0
- package/templates/phaser-game/src/env.d.ts +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 +13 -1
- 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 +59 -26
- package/templates/phaser-game/src/platform/buildGateways/reddit.ts +36 -1
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +39 -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 +22 -0
- 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
|
|
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.
|
|
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__",
|
|
@@ -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
|
-
|
|
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({
|
|
19
|
+
fallbackBridge: createAitSandboxBridge({
|
|
20
|
+
appName: __MPGD_AIT_APP_NAME__,
|
|
21
|
+
buildId: runtime.buildId,
|
|
22
|
+
}),
|
|
11
23
|
});
|
|
12
24
|
}
|
|
@@ -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,70 @@
|
|
|
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 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
|
}
|