@mpgd/cli 0.35.0 → 0.36.1

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 (53) hide show
  1. package/dist/android-bundle-signer.d.ts +2 -0
  2. package/dist/android-bundle-signer.js +38 -0
  3. package/dist/android-signing-session.d.ts +22 -0
  4. package/dist/android-signing-session.js +198 -0
  5. package/dist/artifact-inspection-environment.d.ts +2 -0
  6. package/dist/artifact-inspection-environment.js +31 -0
  7. package/dist/asc-pin.json +26 -0
  8. package/dist/capacitor-shell-starter.d.ts +22 -0
  9. package/dist/capacitor-shell-starter.js +130 -7
  10. package/dist/deploy-planning.d.ts +64 -0
  11. package/dist/deploy-planning.js +471 -0
  12. package/dist/deploy-process.d.ts +28 -0
  13. package/dist/deploy-process.js +197 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.js +409 -10
  16. package/dist/ios-ipa-inspection.js +151 -0
  17. package/dist/ios-keychain-import.swift +92 -0
  18. package/dist/ios-plist.d.ts +2 -0
  19. package/dist/ios-plist.js +75 -0
  20. package/dist/ios-signing-profile.d.ts +12 -0
  21. package/dist/ios-signing-profile.js +60 -0
  22. package/dist/ios-signing-session.d.ts +22 -0
  23. package/dist/ios-signing-session.js +256 -0
  24. package/dist/kit-upgrade-target-readiness.d.ts +8 -0
  25. package/dist/kit-upgrade-target-readiness.js +119 -0
  26. package/dist/kit-upgrade.d.ts +2 -0
  27. package/dist/kit-upgrade.js +3 -0
  28. package/dist/native-build-info.json +5 -0
  29. package/dist/native-build-target.js +12121 -0
  30. package/dist/native-deploy-run.d.ts +35 -0
  31. package/dist/native-deploy-run.js +462 -0
  32. package/dist/native-deploy-submission.d.ts +40 -0
  33. package/dist/native-deploy-submission.js +244 -0
  34. package/dist/native-deploy-workflow.d.ts +11 -0
  35. package/dist/native-deploy-workflow.js +328 -0
  36. package/dist/play-internal-submission.d.ts +32 -0
  37. package/dist/play-internal-submission.js +247 -0
  38. package/dist/play-publisher-port.d.ts +30 -0
  39. package/dist/play-publisher-port.js +1 -0
  40. package/dist/play-sdk-adapter.js +78 -0
  41. package/dist/release-state.d.ts +140 -0
  42. package/dist/release-state.js +1046 -0
  43. package/dist/release-workspace.d.ts +61 -0
  44. package/dist/release-workspace.js +504 -0
  45. package/dist/testflight-submission.d.ts +49 -0
  46. package/dist/testflight-submission.js +462 -0
  47. package/package.json +18 -13
  48. package/templates/phaser-game/README.md +30 -8
  49. package/templates/phaser-game/apps/target-devvit/README.md +12 -0
  50. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +513 -0
  51. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +8 -291
  52. package/templates/phaser-game/src/main.ts +34 -33
  53. package/templates/phaser-game/vite.shared.ts +5 -1
@@ -2,22 +2,20 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
 
3
3
  import { context, createServer, getServerPort, reddit, redis } from '@devvit/web/server';
4
4
  import type { UiResponse } from '@devvit/web/shared';
5
- import {
6
- bridgeStorageLoadProtocol,
7
- createBridgeError,
8
- type BridgeRequest,
9
- type BridgeResponse,
10
- type BridgeStorageLoadData,
11
- } from '@mpgd/bridge';
12
5
  import { createBridgeRpcRouter, defaultBridgeRpcEndpoint } from '@mpgd/bridge/orpc';
13
6
  import { createBridgeRpcNodeHandler } from '@mpgd/bridge/orpc/node';
14
7
 
15
- const maxStorageKeyLength = 128;
16
- const maxEncodedStorageKeyLength = 384;
17
- const maxStorageValueBytes = 262_144;
8
+ import { createDevvitBridgeHandler } from './bridge.js';
9
+
18
10
  const maxRequestBodySize = 1_048_576;
19
11
  const gameName = '__GAME_NAME__';
20
12
  const gameTitle = __GAME_TITLE_TS_LITERAL__;
13
+ const handleBridgeRequest = createDevvitBridgeHandler({
14
+ redis,
15
+ currentPlayerId,
16
+ currentDisplayName,
17
+ storageKeyNamespace: gameName,
18
+ });
21
19
  const bridgeRpcHandler = createBridgeRpcNodeHandler(createBridgeRpcRouter(handleBridgeRequest), {
22
20
  maxBodySize: maxRequestBodySize,
23
21
  prefix: defaultBridgeRpcEndpoint,
@@ -126,279 +124,6 @@ server.listen(port, () => {
126
124
  console.log(`devvit server listening on ${port}`);
127
125
  });
128
126
 
129
- async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse> {
130
- switch (input.method) {
131
- case 'runtime.getCapabilities':
132
- return ok(input, {
133
- nativeIap: false,
134
- nativeAds: false,
135
- rewardedAds: false,
136
- interstitialAds: false,
137
- nativeLeaderboard: false,
138
- remoteLeaderboard: false,
139
- achievements: false,
140
- cloudSave: true,
141
- socialShare: false,
142
- haptics: false,
143
- localizedContent: true,
144
- });
145
-
146
- case 'identity.getPlayer': {
147
- const playerId = currentPlayerId();
148
-
149
- if (playerId === undefined) {
150
- return ok(input, null);
151
- }
152
-
153
- return ok(input, {
154
- playerId,
155
- displayName: await currentDisplayName(playerId),
156
- });
157
- }
158
-
159
- case 'identity.getSession': {
160
- const playerId = currentPlayerId();
161
-
162
- return ok(
163
- input,
164
- playerId === undefined
165
- ? {
166
- identityLevel: 'guest',
167
- trustLevel: 'local',
168
- }
169
- : {
170
- identityLevel: 'authenticated',
171
- playerId,
172
- trustLevel: 'server-verified',
173
- },
174
- );
175
- }
176
-
177
- case 'identity.requestUpgrade': {
178
- const authenticated = currentPlayerId() !== undefined;
179
-
180
- return ok(input, {
181
- status: authenticated ? 'completed' : 'unavailable',
182
- reloadExpected: false,
183
- });
184
- }
185
-
186
- case 'presentation.getLaunchIntent':
187
- return ok(input, { entry: 'home' });
188
-
189
- case 'presentation.requestGameSurface':
190
- return ok(input, 'unavailable');
191
-
192
- case 'share.share':
193
- return ok(input, { status: 'unavailable' });
194
-
195
- case 'share.readInboundShare':
196
- return ok(input, null);
197
-
198
- case 'notifications.getStatus':
199
- return ok(input, 'approval-required');
200
-
201
- case 'notifications.requestSubscription':
202
- return ok(input, 'unavailable');
203
-
204
- case 'commerce.getProducts':
205
- case 'commerce.getEntitlements':
206
- return ok(input, []);
207
-
208
- case 'commerce.purchase':
209
- return ok(input, {
210
- status: 'cancelled',
211
- entitlementIds: [],
212
- });
213
-
214
- case 'commerce.restore':
215
- return ok(input, {
216
- restoredEntitlements: [],
217
- });
218
-
219
- case 'ads.preload':
220
- return ok(input, {});
221
-
222
- case 'leaderboard.open':
223
- return createBridgeError(
224
- input.id,
225
- 'DEVVIT_LEADERBOARD_OPEN_UNAVAILABLE',
226
- 'Devvit leaderboard display is not implemented yet.',
227
- );
228
-
229
- case 'ads.showRewarded':
230
- return ok(input, {
231
- status: 'unavailable',
232
- rewardGranted: false,
233
- });
234
-
235
- case 'ads.showInterstitial':
236
- return ok(input, {
237
- status: 'unavailable',
238
- });
239
-
240
- case 'leaderboard.submitScore':
241
- return ok(input, {
242
- submitted: false,
243
- });
244
-
245
- case 'storage.load':
246
- return loadStorage(input);
247
-
248
- case 'storage.save':
249
- return saveStorage(input);
250
-
251
- default:
252
- return createBridgeError(
253
- input.id,
254
- 'UNSUPPORTED_METHOD',
255
- `Unsupported Devvit bridge method: ${input.method}`,
256
- );
257
- }
258
- }
259
-
260
- async function loadStorage(input: BridgeRequest): Promise<BridgeResponse> {
261
- const playerId = currentPlayerId();
262
-
263
- if (playerId === undefined) {
264
- return createBridgeError(
265
- input.id,
266
- 'DEVVIT_STORAGE_IDENTITY_REQUIRED',
267
- 'A current Reddit player is required to load storage.',
268
- );
269
- }
270
-
271
- const key = storageKey(input, playerId);
272
-
273
- if (typeof key !== 'string') {
274
- return key;
275
- }
276
-
277
- let stored: string | null | undefined;
278
-
279
- try {
280
- stored = await redis.get(key);
281
- } catch (error) {
282
- console.warn(`devvit storage load failed: ${errorMessage(error)}`);
283
- return createBridgeError(
284
- input.id,
285
- 'DEVVIT_STORAGE_LOAD_FAILED',
286
- 'Devvit storage could not be loaded.',
287
- true,
288
- );
289
- }
290
-
291
- if (stored === undefined || stored === null) {
292
- return ok(input, {
293
- __mpgdBridgeProtocol: bridgeStorageLoadProtocol,
294
- found: false,
295
- } satisfies BridgeStorageLoadData);
296
- }
297
-
298
- try {
299
- return ok(
300
- input,
301
- {
302
- __mpgdBridgeProtocol: bridgeStorageLoadProtocol,
303
- found: true,
304
- value: JSON.parse(stored),
305
- } satisfies BridgeStorageLoadData,
306
- );
307
- } catch {
308
- return createBridgeError(input.id, 'CORRUPTED_STORAGE_VALUE', 'Stored data is not valid JSON.');
309
- }
310
- }
311
-
312
- async function saveStorage(input: BridgeRequest): Promise<BridgeResponse> {
313
- const playerId = currentPlayerId();
314
-
315
- if (playerId === undefined) {
316
- return createBridgeError(
317
- input.id,
318
- 'DEVVIT_STORAGE_IDENTITY_REQUIRED',
319
- 'A current Reddit player is required to save storage.',
320
- );
321
- }
322
-
323
- const key = storageKey(input, playerId);
324
-
325
- if (typeof key !== 'string') {
326
- return key;
327
- }
328
-
329
- const payload = optionalObjectPayload(input.payload) as { readonly value?: unknown };
330
- let serialized: string;
331
-
332
- try {
333
- const candidate = JSON.stringify(payload.value);
334
-
335
- if (typeof candidate !== 'string') {
336
- throw new Error('JSON serialization did not produce a string.');
337
- }
338
-
339
- serialized = candidate;
340
- } catch {
341
- return createBridgeError(
342
- input.id,
343
- 'INVALID_STORAGE_VALUE',
344
- 'Storage values must be JSON serializable.',
345
- );
346
- }
347
-
348
- if (new TextEncoder().encode(serialized).length > maxStorageValueBytes) {
349
- return createBridgeError(
350
- input.id,
351
- 'DEVVIT_STORAGE_QUOTA_EXCEEDED',
352
- `Storage values must not exceed ${String(maxStorageValueBytes)} UTF-8 bytes.`,
353
- );
354
- }
355
-
356
- try {
357
- await redis.set(key, serialized);
358
- } catch (error) {
359
- console.warn(`devvit storage save was not persisted: ${errorMessage(error)}`);
360
- return createBridgeError(
361
- input.id,
362
- 'DEVVIT_STORAGE_SAVE_FAILED',
363
- 'Devvit storage could not be saved.',
364
- true,
365
- );
366
- }
367
-
368
- return ok(input, {
369
- saved: true,
370
- playerId,
371
- });
372
- }
373
-
374
- function ok(input: BridgeRequest, data: unknown): BridgeResponse {
375
- return {
376
- id: input.id,
377
- ok: true,
378
- data,
379
- };
380
- }
381
-
382
- function storageKey(input: BridgeRequest, playerId: string): string | BridgeResponse {
383
- const payload = optionalObjectPayload(input.payload);
384
-
385
- if (typeof payload.key !== 'string' || payload.key.length === 0) {
386
- return createBridgeError(input.id, 'INVALID_STORAGE_KEY', 'Storage key is required.');
387
- }
388
-
389
- if (payload.key.length > maxStorageKeyLength) {
390
- return createBridgeError(input.id, 'INVALID_STORAGE_KEY', 'Storage key is too long.');
391
- }
392
-
393
- const encodedKey = encodeURIComponent(payload.key);
394
-
395
- if (encodedKey.length > maxEncodedStorageKeyLength) {
396
- return createBridgeError(input.id, 'INVALID_STORAGE_KEY', 'Encoded storage key is too long.');
397
- }
398
-
399
- return `${gameName}:save:${encodeURIComponent(playerId)}:${encodedKey}`;
400
- }
401
-
402
127
  function errorMessage(error: unknown): string {
403
128
  return error instanceof Error ? error.message : String(error);
404
129
  }
@@ -435,14 +160,6 @@ function currentSubredditName(): string | undefined {
435
160
  : undefined;
436
161
  }
437
162
 
438
- function optionalObjectPayload(payload: unknown): Record<string, unknown> {
439
- if (typeof payload !== 'object' || payload === null) {
440
- return {};
441
- }
442
-
443
- return payload as Record<string, unknown>;
444
- }
445
-
446
163
  function requestPathname(request: IncomingMessage): string {
447
164
  return new URL(request.url ?? '/', 'http://localhost').pathname;
448
165
  }
@@ -4,7 +4,10 @@ import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/anal
4
4
  import { resolveTargetMpgdLocale, type Locale } from '@mpgd/i18n';
5
5
  import type { IdentitySession, LaunchIntent, PlatformGateway } from '@mpgd/platform';
6
6
  import {
7
+ measureTargetViewport,
7
8
  resolveTargetViewportSnapshot,
9
+ waitForTargetViewportMeasurement,
10
+ type TargetViewportMeasurement,
8
11
  type TargetViewportOrientationPolicy,
9
12
  } from '@mpgd/target-config';
10
13
 
@@ -36,8 +39,13 @@ async function bootstrap(): Promise<void> {
36
39
  const orientationPolicy = {
37
40
  mode: 'prefer-landscape',
38
41
  } as const satisfies TargetViewportOrientationPolicy;
42
+ // A hidden iframe or background tab can boot at 0x0; wait for layout instead of failing.
43
+ const measurement = await waitForTargetViewportMeasurement({
44
+ measure: measureGameViewport,
45
+ subscribe: subscribeToGameViewportChanges,
46
+ });
39
47
  const viewport = resolveTargetViewportSnapshot({
40
- ...measureGameViewport(),
48
+ ...measurement,
41
49
  runtime: runtime.config.runtime,
42
50
  orientationPolicy,
43
51
  });
@@ -152,40 +160,33 @@ function renderBootstrapError(error: unknown, locale: Locale): void {
152
160
  }
153
161
 
154
162
  /** Measure the game surface before falling back to browser viewport geometry. */
155
- function measureGameViewport(): {
156
- readonly width: number;
157
- readonly height: number;
158
- readonly source: 'container' | 'visual-viewport' | 'window';
159
- } {
163
+ function measureGameViewport(): TargetViewportMeasurement | null {
164
+ return measureTargetViewport({
165
+ container: document.querySelector<HTMLElement>('#game'),
166
+ visualViewport: window.visualViewport,
167
+ window,
168
+ });
169
+ }
170
+
171
+ /** Notify when the game surface may have gained a size or become visible. */
172
+ function subscribeToGameViewportChanges(listener: () => void): () => void {
160
173
  const container = document.querySelector<HTMLElement>('#game');
161
- const rect = container?.getBoundingClientRect();
162
-
163
- if (rect !== undefined && rect.width > 0 && rect.height > 0) {
164
- return {
165
- width: rect.width,
166
- height: rect.height,
167
- source: 'container',
168
- };
169
- }
174
+ const observer =
175
+ container === null || typeof ResizeObserver !== 'function'
176
+ ? undefined
177
+ : new ResizeObserver(listener);
170
178
 
171
- const visualViewport = window.visualViewport;
172
-
173
- if (
174
- visualViewport !== undefined &&
175
- visualViewport !== null &&
176
- visualViewport.width > 0 &&
177
- visualViewport.height > 0
178
- ) {
179
- return {
180
- width: visualViewport.width,
181
- height: visualViewport.height,
182
- source: 'visual-viewport',
183
- };
179
+ if (container !== null) {
180
+ observer?.observe(container);
184
181
  }
185
-
186
- return {
187
- width: window.innerWidth,
188
- height: window.innerHeight,
189
- source: 'window',
182
+ window.addEventListener('resize', listener);
183
+ window.visualViewport?.addEventListener('resize', listener);
184
+ document.addEventListener('visibilitychange', listener);
185
+
186
+ return () => {
187
+ observer?.disconnect();
188
+ window.removeEventListener('resize', listener);
189
+ window.visualViewport?.removeEventListener('resize', listener);
190
+ document.removeEventListener('visibilitychange', listener);
190
191
  };
191
192
  }
@@ -18,6 +18,10 @@ interface RuntimePlatformTargetMetadata {
18
18
  readonly integrations?: Record<string, unknown>;
19
19
  }
20
20
 
21
+ // Sandbox gateways self-complete purchases and rewards without any backend
22
+ // verification, so a non-production build alone must never select them. Each
23
+ // one additionally requires the explicit BUILD_ID its dev script sets.
24
+ const aitSandboxBuildId = 'ait-sandbox';
21
25
  const devvitSandboxBuildId = 'devvit-sandbox';
22
26
 
23
27
  export interface CreateGameViteSharedConfigInput {
@@ -140,7 +144,7 @@ export function resolveBuildGatewayModule(input: {
140
144
  case 'ios':
141
145
  return 'src/platform/buildGateways/capacitorIos.ts';
142
146
  case 'ait':
143
- return input.debug
147
+ return input.debug && input.buildId === aitSandboxBuildId
144
148
  ? 'src/platform/buildGateways/aitSandbox.ts'
145
149
  : 'src/platform/buildGateways/ait.ts';
146
150
  case 'reddit':