@mpgd/cli 0.52.0 → 0.53.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 +22 -0
- package/dist/capacitor-shell-starter.js +387 -18
- package/dist/index.js +35 -4
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +363 -2
- package/dist/notifications-starter.d.ts +69 -0
- package/dist/notifications-starter.js +513 -0
- package/package.json +17 -17
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +75 -0
- package/templates/notifications-overlay/common/notifications/deployment.json +8 -0
- package/templates/notifications-overlay/common/notifications/topics.json +222 -0
- package/templates/notifications-overlay/common/src/platform/capacitorNotifications.ts +59 -0
- package/templates/notifications-overlay/common/src/platform/notificationOpens.ts +60 -0
- package/templates/notifications-overlay/common/src/platform/notificationSettings.ts +761 -0
- package/templates/notifications-overlay/common/src/platform/notifications.ts +131 -0
- package/templates/notifications-overlay/common/tools/validate-notifications.ts +75 -0
- package/templates/notifications-overlay/lite/src/lite/notificationSettingsPanel.ts +200 -0
- package/templates/notifications-overlay/phaser/src/scenes/NotificationSettingsScene.ts +282 -0
- package/templates/phaser-game/apps/target-ait/src/main.ts +15 -0
- package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +5 -0
- package/templates/phaser-game/apps/target-ait/vite.config.ts +22 -1
- package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +14 -0
|
@@ -67,6 +67,15 @@ export interface CapacitorShellStarterPlan {
|
|
|
67
67
|
/** Native store plugins the plan installs into the shell and declares in the game. */
|
|
68
68
|
readonly providerPlugins: readonly CapacitorShellPlannedProviderPlugin[];
|
|
69
69
|
/**
|
|
70
|
+
* The notifications plugins (`--providers notifications`) the plan installs
|
|
71
|
+
* into the shell, for Android and iOS, at the game's versions; empty when
|
|
72
|
+
* notifications are not requested.
|
|
73
|
+
*/
|
|
74
|
+
readonly notificationPlugins: readonly {
|
|
75
|
+
readonly packageName: string;
|
|
76
|
+
readonly version: string;
|
|
77
|
+
}[];
|
|
78
|
+
/**
|
|
70
79
|
* Store wiring the plan could not write or confirm: a missing or game-owned
|
|
71
80
|
* gateway that does not register the plugin, or a store plugin the shell
|
|
72
81
|
* keeps although it is no longer requested. Printed; game source is never
|
|
@@ -115,4 +124,17 @@ export declare function assertAndroidReleaseLauncherIntegrity(shellApp: string):
|
|
|
115
124
|
export declare function assertIosReleaseDisplayName(shellApp: string, expectedDisplayName: string): void;
|
|
116
125
|
export declare function assertIosArchiveScheme(shellApp: string): void;
|
|
117
126
|
export declare function decodeAndroidStringResource(value: string): string;
|
|
127
|
+
/** The permission line that drops `SCHEDULE_EXACT_ALARM` from the merged Android manifest. */
|
|
128
|
+
export declare const androidInexactAlarmPermission = "<uses-permission android:name=\"android.permission.SCHEDULE_EXACT_ALARM\" tools:node=\"remove\" />";
|
|
129
|
+
/** Whether an Android manifest already drops `SCHEDULE_EXACT_ALARM` from the merge. */
|
|
130
|
+
export declare function androidManifestRemovesExactAlarm(manifest: string): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Drops `SCHEDULE_EXACT_ALARM`, which `@capacitor/local-notifications` adds
|
|
133
|
+
* to the merged manifest: reminders are inexact (notifications RFC
|
|
134
|
+
* "Reminders"), Android 14 denies the permission to new installs, and Play
|
|
135
|
+
* restricts it to alarm and calendar apps. The tools namespace is bound on
|
|
136
|
+
* the manifest root when missing. A manifest that already drops it is
|
|
137
|
+
* returned unchanged.
|
|
138
|
+
*/
|
|
139
|
+
export declare function applyAndroidManifestInexactAlarms(manifest: string): string;
|
|
118
140
|
export {};
|
|
@@ -4,7 +4,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { DOMParser } from '@xmldom/xmldom';
|
|
7
|
-
import { satisfies, valid } from 'semver';
|
|
7
|
+
import { satisfies, valid, validRange } from 'semver';
|
|
8
|
+
import { capacitorGatewayRegistersNotifications, conflictingPushPlugin, notificationsCapacitorGatewayModules, notificationsCapacitorGateways, notificationsCapacitorSupportFiles, notificationsGameDependencies, notificationsMessagingFirebaseRange, notificationsMessagingPlugin, notificationsProviderId, notificationsShellPlugins, wireCapacitorGatewayNotifications, } from './notifications-starter.js';
|
|
8
9
|
import { isNonPublicServiceHostname } from './production-target-readiness.js';
|
|
9
10
|
import { assertAndroidSettingsAppProject, assertAndroidSettingsNoAppRemap, assertIosReleaseInfoPlistExpansion, assertIosReleasePackagingSettings, assertIosReleaseProductName, assertNativeShellIdentity, hasAndroidDisplayNameResourceOverride, hasAndroidManifestSourceSetOverride, hasAndroidResourceSourceSetOverride, hasGradleIdentityMutation, hasGradleTaskAction, maskGradleStrings, readIosAppTargetId, readIosReleaseInfoPlist, stripGradleComments, } from './native-shell-identity.js';
|
|
10
11
|
const shellPath = 'apps/mobile-capacitor';
|
|
@@ -239,7 +240,24 @@ export function planCapacitorShellStarter(input) {
|
|
|
239
240
|
.filter((plugin) => requestedProviderIds.includes(plugin.id))
|
|
240
241
|
.map((plugin) => resolveProviderPlugin(gameRoot, gameDependencies, cliDevelopmentDependencies, plugin));
|
|
241
242
|
const warnings = [];
|
|
242
|
-
const
|
|
243
|
+
const storeGatewayFiles = planProviderGatewayWiring(gameRoot, input.templateRoot, providerPlugins, warnings);
|
|
244
|
+
const notifications = requestedProviderIds.includes(notificationsProviderId)
|
|
245
|
+
? planNotificationsProvider({
|
|
246
|
+
gameRoot,
|
|
247
|
+
templateRoot: input.templateRoot,
|
|
248
|
+
displayName: input.displayName,
|
|
249
|
+
gameDependencies,
|
|
250
|
+
existingShellPackage: packageContent,
|
|
251
|
+
plannedGateways: storeGatewayFiles,
|
|
252
|
+
warnings,
|
|
253
|
+
})
|
|
254
|
+
: undefined;
|
|
255
|
+
// The notifications wiring builds on the store wiring of the same gateway.
|
|
256
|
+
const gatewayFiles = [
|
|
257
|
+
...storeGatewayFiles.filter((file) => notifications?.files.some((planned) => planned.path === file.path) !== true),
|
|
258
|
+
...(notifications?.files ?? []),
|
|
259
|
+
];
|
|
260
|
+
const notificationPlugins = notifications?.shellPlugins ?? [];
|
|
243
261
|
if (packageContent !== undefined) {
|
|
244
262
|
const existingShellDependencies = requireObject(parseJsonObject(packageContent, 'existing shell package').dependencies, 'existing shell dependencies');
|
|
245
263
|
for (const plugin of capacitorShellProviderPlugins) {
|
|
@@ -250,6 +268,35 @@ export function planCapacitorShellStarter(input) {
|
|
|
250
268
|
+ 'shell package and resync if the game stopped selling there.');
|
|
251
269
|
}
|
|
252
270
|
}
|
|
271
|
+
if (notifications === undefined) {
|
|
272
|
+
for (const name of notificationsShellPlugins) {
|
|
273
|
+
if (existingShellDependencies[name] !== undefined) {
|
|
274
|
+
warnings.push(`The shell still lists ${name} although ${notificationsProviderId} is no longer `
|
|
275
|
+
+ 'requested; the initializer never removes a plugin, so remove it from the shell '
|
|
276
|
+
+ 'package and resync if the game stopped sending notifications.');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (notifications !== undefined) {
|
|
282
|
+
// The starter exposes notifications on both platforms; the provider still
|
|
283
|
+
// reports its own readiness (no plugin, no Firebase files), which the
|
|
284
|
+
// target config follows. A state the game set itself is kept.
|
|
285
|
+
for (const targetName of ['android', 'ios']) {
|
|
286
|
+
const target = requireObject(targetMap[targetName], `${targetName} target`);
|
|
287
|
+
const integrations = target.integrations === undefined
|
|
288
|
+
? {}
|
|
289
|
+
: requireObject(target.integrations, `${targetName}.integrations`);
|
|
290
|
+
if (integrations.notifications === undefined) {
|
|
291
|
+
target.integrations = { ...integrations, notifications: 'available' };
|
|
292
|
+
}
|
|
293
|
+
else if (integrations.notifications !== 'available'
|
|
294
|
+
&& integrations.notifications !== 'action-required') {
|
|
295
|
+
warnings.push(`mpgd.targets.json keeps ${targetName}.integrations.notifications at `
|
|
296
|
+
+ `${String(integrations.notifications)}, which hides notifications from the game; set it `
|
|
297
|
+
+ 'to available once the shell is configured.');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
253
300
|
}
|
|
254
301
|
const shellDependencies = {
|
|
255
302
|
'@capacitor/android': capacitorVersion,
|
|
@@ -258,6 +305,7 @@ export function planCapacitorShellStarter(input) {
|
|
|
258
305
|
'@capacitor/ios': capacitorVersion,
|
|
259
306
|
'@mpgd/capacitor-game-services': pluginVersion,
|
|
260
307
|
...Object.fromEntries(providerPlugins.map((plugin) => [plugin.packageName, plugin.version])),
|
|
308
|
+
...Object.fromEntries(notificationPlugins.map((plugin) => [plugin.packageName, plugin.version])),
|
|
261
309
|
};
|
|
262
310
|
if (packageContent !== undefined) {
|
|
263
311
|
assertCompatibleExistingPackage(packageContent, shellDependencies, capacitorVersion, typescriptVersion);
|
|
@@ -304,7 +352,7 @@ export function planCapacitorShellStarter(input) {
|
|
|
304
352
|
typescript: typescriptVersion,
|
|
305
353
|
},
|
|
306
354
|
})
|
|
307
|
-
: withPackageDependencies(packageContent, providerPlugins),
|
|
355
|
+
: withPackageDependencies(packageContent, [...providerPlugins, ...notificationPlugins]),
|
|
308
356
|
},
|
|
309
357
|
{
|
|
310
358
|
path: `${shellPath}/capacitor.config.ts`,
|
|
@@ -331,17 +379,27 @@ export function planCapacitorShellStarter(input) {
|
|
|
331
379
|
appId: input.appId,
|
|
332
380
|
displayName: input.displayName,
|
|
333
381
|
requestedProviderIds,
|
|
334
|
-
...(providerPlugins.length === 0
|
|
382
|
+
...(providerPlugins.length === 0 && notificationPlugins.length === 0
|
|
335
383
|
? {}
|
|
336
384
|
: {
|
|
337
|
-
providers:
|
|
338
|
-
plugin
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
385
|
+
providers: {
|
|
386
|
+
...Object.fromEntries(providerPlugins.map((plugin) => [
|
|
387
|
+
plugin.id,
|
|
388
|
+
{
|
|
389
|
+
package: plugin.packageName,
|
|
390
|
+
version: plugin.version,
|
|
391
|
+
platform: plugin.platform,
|
|
392
|
+
},
|
|
393
|
+
])),
|
|
394
|
+
...(notificationPlugins.length === 0
|
|
395
|
+
? {}
|
|
396
|
+
: {
|
|
397
|
+
[notificationsProviderId]: {
|
|
398
|
+
packages: Object.fromEntries(notificationPlugins.map((plugin) => [plugin.packageName, plugin.version])),
|
|
399
|
+
platforms: ['android', 'ios'],
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
},
|
|
345
403
|
}),
|
|
346
404
|
...(requestedBackendUrl === undefined ? {} : { backendUrl: requestedBackendUrl }),
|
|
347
405
|
}),
|
|
@@ -361,7 +419,12 @@ export function planCapacitorShellStarter(input) {
|
|
|
361
419
|
'',
|
|
362
420
|
'requestedProviderIds is selection metadata, except play-billing and',
|
|
363
421
|
'storekit: those install the kit store plugin into this shell and the game',
|
|
364
|
-
'and register it with cap sync
|
|
422
|
+
'and register it with cap sync; notifications installs',
|
|
423
|
+
'@capacitor-firebase/messaging and @capacitor/local-notifications (never',
|
|
424
|
+
`${conflictingPushPlugin}, which claims the same iOS push handler).`,
|
|
425
|
+
'Notifications also need the Firebase files (google-services.json,',
|
|
426
|
+
'GoogleService-Info.plist) and the iOS Push Notifications capability; see',
|
|
427
|
+
'the mpgd-kit Firebase guide. A plugin never reports a feature as',
|
|
365
428
|
'available on its own; the gateway registers the store provider, and the',
|
|
366
429
|
'store answers configuration-required until the plugin and products exist.',
|
|
367
430
|
'Production builds require a real game-owned backend when grants are enabled.',
|
|
@@ -415,16 +478,33 @@ export function planCapacitorShellStarter(input) {
|
|
|
415
478
|
// The game bundle imports the plugin from its gateway file, so the game
|
|
416
479
|
// declares it too, at the shell's exact version; the materializer's game
|
|
417
480
|
// install picks it up.
|
|
418
|
-
const missingGameDependencies =
|
|
419
|
-
|
|
481
|
+
const missingGameDependencies = [
|
|
482
|
+
...providerPlugins.map((plugin) => [plugin.packageName, plugin.version]),
|
|
483
|
+
...Object.entries(notifications?.gameDependencies ?? {}),
|
|
484
|
+
].filter(([name]) => gameDependencies[name] === undefined);
|
|
485
|
+
const gameScripts = gamePackage.scripts === undefined
|
|
486
|
+
? {}
|
|
487
|
+
: requireObject(gamePackage.scripts, 'game scripts');
|
|
488
|
+
const validatorPresent = notifications !== undefined
|
|
489
|
+
&& (notifications.files.some((file) => file.path === 'tools/validate-notifications.ts')
|
|
490
|
+
|| readExisting(safeDestination(gameRoot, 'tools/validate-notifications.ts')) !== undefined);
|
|
491
|
+
const missingScripts = !validatorPresent || gameScripts['validate:notifications'] !== undefined
|
|
492
|
+
? {}
|
|
493
|
+
: {
|
|
494
|
+
'validate:notifications': 'ttsx --project tsconfig.tools.json tools/validate-notifications.ts',
|
|
495
|
+
};
|
|
496
|
+
if (missingGameDependencies.length > 0 || Object.keys(missingScripts).length > 0) {
|
|
420
497
|
safeDestination(gameRoot, 'package.json');
|
|
421
498
|
requestedFiles.push({
|
|
422
499
|
path: 'package.json',
|
|
423
500
|
content: json({
|
|
424
501
|
...gamePackage,
|
|
502
|
+
...(Object.keys(missingScripts).length === 0
|
|
503
|
+
? {}
|
|
504
|
+
: { scripts: { ...gameScripts, ...missingScripts } }),
|
|
425
505
|
dependencies: {
|
|
426
506
|
...gameDependencies,
|
|
427
|
-
...Object.fromEntries(missingGameDependencies
|
|
507
|
+
...Object.fromEntries(missingGameDependencies),
|
|
428
508
|
},
|
|
429
509
|
}),
|
|
430
510
|
});
|
|
@@ -454,6 +534,7 @@ export function planCapacitorShellStarter(input) {
|
|
|
454
534
|
changedFiles: files.map((file) => file.path),
|
|
455
535
|
nativePlatformsToAdd,
|
|
456
536
|
providerPlugins,
|
|
537
|
+
notificationPlugins,
|
|
457
538
|
warnings,
|
|
458
539
|
};
|
|
459
540
|
}
|
|
@@ -570,6 +651,9 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
|
|
|
570
651
|
const manifest = readJsonObject(path.join(plan.gameRoot, shellPath, 'mpgd.native-shell.json'));
|
|
571
652
|
if (platform === 'android' && generatedNow) {
|
|
572
653
|
materializeAndroidBackupPolicy(plan.gameRoot);
|
|
654
|
+
if (plan.notificationPlugins.length > 0) {
|
|
655
|
+
materializeAndroidInexactAlarms(plan.gameRoot);
|
|
656
|
+
}
|
|
573
657
|
}
|
|
574
658
|
if (platform === 'ios') {
|
|
575
659
|
materializeIosArchiveScheme(plan.gameRoot);
|
|
@@ -588,7 +672,8 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
|
|
|
588
672
|
// `cap add` syncs a new project itself; a preserved project needs an explicit
|
|
589
673
|
// sync so the store plugin's native code joins the shell.
|
|
590
674
|
for (const platform of ['android', 'ios']) {
|
|
591
|
-
if (plan.providerPlugins.some((plugin) => plugin.platform === platform)
|
|
675
|
+
if ((plan.providerPlugins.some((plugin) => plugin.platform === platform)
|
|
676
|
+
|| plan.notificationPlugins.length > 0)
|
|
592
677
|
&& !plan.nativePlatformsToAdd.includes(platform)
|
|
593
678
|
&& existsSync(path.join(plan.gameRoot, shellPath, platform))) {
|
|
594
679
|
runner.run('pnpm', ['--dir', shellPath, 'cap', 'sync', platform], plan.gameRoot);
|
|
@@ -2043,7 +2128,10 @@ function assertCompatibleExistingPackage(source, shellDependencies, capacitorVer
|
|
|
2043
2128
|
throw new Error('Existing Capacitor shell package must stay private.');
|
|
2044
2129
|
}
|
|
2045
2130
|
const dependencies = requireObject(value.dependencies, 'existing shell dependencies');
|
|
2046
|
-
const providerPackages = new Set(
|
|
2131
|
+
const providerPackages = new Set([
|
|
2132
|
+
...capacitorShellProviderPlugins.map((plugin) => plugin.packageName),
|
|
2133
|
+
...notificationsShellPlugins,
|
|
2134
|
+
]);
|
|
2047
2135
|
for (const [name, version] of Object.entries(shellDependencies)) {
|
|
2048
2136
|
// A store plugin the shell does not list yet is added by the plan. One it
|
|
2049
2137
|
// lists at another version would compile native code that does not match
|
|
@@ -2472,6 +2560,287 @@ function planProviderGatewayWiring(gameRoot, templateRoot, plugins, warnings) {
|
|
|
2472
2560
|
}
|
|
2473
2561
|
return [...planned].map(([file, content]) => ({ path: file, content }));
|
|
2474
2562
|
}
|
|
2563
|
+
/** The permission line that drops `SCHEDULE_EXACT_ALARM` from the merged Android manifest. */
|
|
2564
|
+
export const androidInexactAlarmPermission = '<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" tools:node="remove" />';
|
|
2565
|
+
/** Whether an Android manifest already drops `SCHEDULE_EXACT_ALARM` from the merge. */
|
|
2566
|
+
export function androidManifestRemovesExactAlarm(manifest) {
|
|
2567
|
+
const code = withoutXmlComments(manifest);
|
|
2568
|
+
return /<uses-permission\b[^>]*android:name\s*=\s*"android\.permission\.SCHEDULE_EXACT_ALARM"[^>]*tools:node\s*=\s*"remove"/u
|
|
2569
|
+
.test(code)
|
|
2570
|
+
|| /<uses-permission\b[^>]*tools:node\s*=\s*"remove"[^>]*android:name\s*=\s*"android\.permission\.SCHEDULE_EXACT_ALARM"/u
|
|
2571
|
+
.test(code);
|
|
2572
|
+
}
|
|
2573
|
+
/** The manifest with each XML comment blanked to spaces, so offsets stay the same. */
|
|
2574
|
+
function withoutXmlComments(manifest) {
|
|
2575
|
+
return manifest.replace(/<!--[\s\S]*?-->/gu, (comment) => comment.replace(/[^\n]/gu, ' '));
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Drops `SCHEDULE_EXACT_ALARM`, which `@capacitor/local-notifications` adds
|
|
2579
|
+
* to the merged manifest: reminders are inexact (notifications RFC
|
|
2580
|
+
* "Reminders"), Android 14 denies the permission to new installs, and Play
|
|
2581
|
+
* restricts it to alarm and calendar apps. The tools namespace is bound on
|
|
2582
|
+
* the manifest root when missing. A manifest that already drops it is
|
|
2583
|
+
* returned unchanged.
|
|
2584
|
+
*/
|
|
2585
|
+
export function applyAndroidManifestInexactAlarms(manifest) {
|
|
2586
|
+
if (androidManifestRemovesExactAlarm(manifest)) {
|
|
2587
|
+
return manifest;
|
|
2588
|
+
}
|
|
2589
|
+
const code = withoutXmlComments(manifest);
|
|
2590
|
+
const root = /<manifest\b[^>]*?>/u.exec(code);
|
|
2591
|
+
const close = code.lastIndexOf('</manifest>');
|
|
2592
|
+
if (root === null || close === -1 || root[0].endsWith('/>') || close < root.index) {
|
|
2593
|
+
return manifest;
|
|
2594
|
+
}
|
|
2595
|
+
const newline = manifest.includes('\r\n') ? '\r\n' : '\n';
|
|
2596
|
+
let rootTag = manifest.slice(root.index, root.index + root[0].length);
|
|
2597
|
+
if (!/\sxmlns:tools\s*=/u.test(rootTag)) {
|
|
2598
|
+
rootTag = rootTag.replace(/^<manifest\b/u, `<manifest xmlns:tools="${androidToolsNamespace}"`);
|
|
2599
|
+
}
|
|
2600
|
+
const head = `${manifest.slice(0, root.index)}${rootTag}${manifest.slice(root.index + root[0].length, close)}`;
|
|
2601
|
+
return `${head.trimEnd()}${newline} ${androidInexactAlarmPermission}${newline}${manifest.slice(close)}`;
|
|
2602
|
+
}
|
|
2603
|
+
function materializeAndroidInexactAlarms(gameRoot) {
|
|
2604
|
+
const manifestFile = safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`);
|
|
2605
|
+
if (!existsSync(manifestFile) || !lstatSync(manifestFile).isFile()) {
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const source = readFileSync(manifestFile, 'utf8');
|
|
2609
|
+
const next = applyAndroidManifestInexactAlarms(source);
|
|
2610
|
+
if (next !== source) {
|
|
2611
|
+
writeFileSync(manifestFile, next);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Plans `--providers notifications` for the shell and the game: the two
|
|
2616
|
+
* Capacitor plugins at the game's installed versions (or the starter's), the
|
|
2617
|
+
* game's notifications packages, and, when a Capacitor gateway is still the
|
|
2618
|
+
* template's, its notifications wiring with the starter modules it imports.
|
|
2619
|
+
* A game-owned gateway is only checked. Native setup the initializer never
|
|
2620
|
+
* writes (Firebase files, the iOS capability, an existing Android manifest)
|
|
2621
|
+
* is reported as warnings.
|
|
2622
|
+
*/
|
|
2623
|
+
function planNotificationsProvider(input) {
|
|
2624
|
+
const { gameRoot, gameDependencies, warnings } = input;
|
|
2625
|
+
const shellDependencies = input.existingShellPackage === undefined
|
|
2626
|
+
? {}
|
|
2627
|
+
: requireObject(parseJsonObject(input.existingShellPackage, 'existing shell package').dependencies, 'existing shell dependencies');
|
|
2628
|
+
if (gameDependencies[conflictingPushPlugin] !== undefined
|
|
2629
|
+
|| shellDependencies[conflictingPushPlugin] !== undefined) {
|
|
2630
|
+
throw new Error(`--providers notifications installs @capacitor-firebase/messaging, which cannot run next to `
|
|
2631
|
+
+ `${conflictingPushPlugin}: on iOS both claim Capacitor's one push handler. Remove `
|
|
2632
|
+
+ `${conflictingPushPlugin} from the game and the shell first.`);
|
|
2633
|
+
}
|
|
2634
|
+
const versionOf = (name) => {
|
|
2635
|
+
const installedFile = path.join(gameRoot, 'node_modules', name, 'package.json');
|
|
2636
|
+
if (existsSync(installedFile)) {
|
|
2637
|
+
const installed = readJsonObject(installedFile);
|
|
2638
|
+
const version = requireString(installed.version, `installed ${name} version`);
|
|
2639
|
+
if (valid(version) === null) {
|
|
2640
|
+
throw new Error(`${name} must resolve to a concrete version, not ${version}.`);
|
|
2641
|
+
}
|
|
2642
|
+
// The plugin's own peers (Capacitor core) must accept the game's
|
|
2643
|
+
// installed copies, as a store plugin's must.
|
|
2644
|
+
assertInstalledPeers(gameRoot, `${name}@${version}`, installed.peerDependencies);
|
|
2645
|
+
return { version, installed: true };
|
|
2646
|
+
}
|
|
2647
|
+
if (gameDependencies[name] !== undefined) {
|
|
2648
|
+
throw new Error(`The game declares ${name} but it is not installed; install game dependencies before `
|
|
2649
|
+
+ 'initializing the shell.');
|
|
2650
|
+
}
|
|
2651
|
+
const version = notificationsGameDependencies[name];
|
|
2652
|
+
if (version === undefined) {
|
|
2653
|
+
throw new Error(`No starter version is known for ${name}.`);
|
|
2654
|
+
}
|
|
2655
|
+
return { version, installed: false };
|
|
2656
|
+
};
|
|
2657
|
+
const shellPlugins = notificationsShellPlugins.map((name) => ({
|
|
2658
|
+
packageName: name,
|
|
2659
|
+
...versionOf(name),
|
|
2660
|
+
}));
|
|
2661
|
+
// The wiring imports @mpgd/adapter-capacitor/notifications: the game's
|
|
2662
|
+
// installed adapter must have it, and the plugins must satisfy its peers.
|
|
2663
|
+
const adapter = readJsonObject(path.join(gameRoot, 'node_modules/@mpgd/adapter-capacitor/package.json'));
|
|
2664
|
+
const adapterExports = adapter.exports;
|
|
2665
|
+
if (typeof adapterExports !== 'object' || adapterExports === null
|
|
2666
|
+
|| adapterExports['./notifications'] === undefined) {
|
|
2667
|
+
throw new Error(`--providers notifications needs @mpgd/adapter-capacitor/notifications, which the game's `
|
|
2668
|
+
+ `@mpgd/adapter-capacitor ${String(adapter.version)} does not export; upgrade the game's kit `
|
|
2669
|
+
+ 'packages (mpgd kit upgrade) first.');
|
|
2670
|
+
}
|
|
2671
|
+
// The starter's provider module also imports the settings opener, which
|
|
2672
|
+
// an adapter older than this CLI's notifications starter does not have.
|
|
2673
|
+
// The subpath's declared types file (dist/notifications.d.ts today), and
|
|
2674
|
+
// the module it re-exports the opener from.
|
|
2675
|
+
const adapterRoot = path.join(gameRoot, 'node_modules/@mpgd/adapter-capacitor');
|
|
2676
|
+
const declaredTypes = adapterExports['./notifications'];
|
|
2677
|
+
const typesPath = typeof declaredTypes === 'object' && declaredTypes !== null
|
|
2678
|
+
&& typeof declaredTypes.types === 'string'
|
|
2679
|
+
? declaredTypes.types
|
|
2680
|
+
: './dist/notifications.d.ts';
|
|
2681
|
+
const adapterTypes = readExisting(path.join(adapterRoot, typesPath));
|
|
2682
|
+
const openerTypes = readExisting(path.join(adapterRoot, path.dirname(typesPath), 'notifications-settings.d.ts'));
|
|
2683
|
+
// Refused only when the declarations show the opener is absent: an
|
|
2684
|
+
// `export *` the check cannot follow passes, and the build then decides.
|
|
2685
|
+
if (adapterTypes !== undefined && !/\bexport\s*\*/u.test(adapterTypes)
|
|
2686
|
+
&& !`${adapterTypes}\n${openerTypes ?? ''}`.includes('openCapacitorNotificationSettings')) {
|
|
2687
|
+
throw new Error(`The game's @mpgd/adapter-capacitor ${String(adapter.version)} has no `
|
|
2688
|
+
+ 'openCapacitorNotificationSettings, which the notifications starter uses; upgrade the '
|
|
2689
|
+
+ "game's kit packages (mpgd kit upgrade) first.");
|
|
2690
|
+
}
|
|
2691
|
+
// The adapter's other peers (Capacitor core) must accept the game's
|
|
2692
|
+
// installed copies too; the plugins get their own message below.
|
|
2693
|
+
assertInstalledPeers(gameRoot, `@mpgd/adapter-capacitor@${String(adapter.version)}`, typeof adapter.peerDependencies === 'object' && adapter.peerDependencies !== null
|
|
2694
|
+
? Object.fromEntries(Object.entries(adapter.peerDependencies)
|
|
2695
|
+
.filter(([name]) => !notificationsShellPlugins.includes(name)))
|
|
2696
|
+
: undefined);
|
|
2697
|
+
const adapterPeers = typeof adapter.peerDependencies === 'object' && adapter.peerDependencies !== null
|
|
2698
|
+
? adapter.peerDependencies
|
|
2699
|
+
: {};
|
|
2700
|
+
for (const plugin of shellPlugins) {
|
|
2701
|
+
const range = adapterPeers[plugin.packageName];
|
|
2702
|
+
// A range semver cannot read (`workspace:*`, a tag) is not checked.
|
|
2703
|
+
if (typeof range === 'string' && validRange(range) !== null
|
|
2704
|
+
&& !satisfies(plugin.version, range, { includePrerelease: true })) {
|
|
2705
|
+
throw new Error(plugin.installed
|
|
2706
|
+
? `@mpgd/adapter-capacitor ${String(adapter.version)} needs ${plugin.packageName} ${range}, `
|
|
2707
|
+
+ `but the game has ${plugin.version}; align the game's version before rerunning.`
|
|
2708
|
+
: `@mpgd/adapter-capacitor ${String(adapter.version)} needs ${plugin.packageName} ${range}, `
|
|
2709
|
+
+ `but this CLI would add ${plugin.version}; add a version in that range to the game `
|
|
2710
|
+
+ `(pnpm add ${plugin.packageName}) or upgrade the CLI, then rerun.`);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
// The messaging plugin's web fallback imports `firebase`: the game's own
|
|
2714
|
+
// copy must be in the plugin's peer range. For the plugin the starter
|
|
2715
|
+
// adds, the kit pins the range. An installed plugin names its own, which
|
|
2716
|
+
// `versionOf` already checked against an installed firebase; this covers
|
|
2717
|
+
// the firebase the CLI would add.
|
|
2718
|
+
const messaging = shellPlugins.find((plugin) => plugin.packageName === notificationsMessagingPlugin);
|
|
2719
|
+
const firebase = versionOf('firebase');
|
|
2720
|
+
if (messaging !== undefined) {
|
|
2721
|
+
const installedPeers = messaging.installed
|
|
2722
|
+
? readJsonObject(path.join(gameRoot, 'node_modules', notificationsMessagingPlugin, 'package.json')).peerDependencies
|
|
2723
|
+
: undefined;
|
|
2724
|
+
const declared = typeof installedPeers === 'object' && installedPeers !== null
|
|
2725
|
+
? installedPeers['firebase']
|
|
2726
|
+
: undefined;
|
|
2727
|
+
const firebaseRange = messaging.installed ? declared : notificationsMessagingFirebaseRange;
|
|
2728
|
+
if (typeof firebaseRange === 'string' && validRange(firebaseRange) !== null
|
|
2729
|
+
&& !satisfies(firebase.version, firebaseRange, { includePrerelease: true })) {
|
|
2730
|
+
throw new Error(firebase.installed
|
|
2731
|
+
? `@capacitor-firebase/messaging ${messaging.version} needs firebase ${firebaseRange}, but the game `
|
|
2732
|
+
+ `has ${firebase.version}; align the game's firebase (pnpm add firebase@"${firebaseRange}") `
|
|
2733
|
+
+ 'before rerunning.'
|
|
2734
|
+
: `@capacitor-firebase/messaging ${messaging.version} needs firebase ${firebaseRange}, but this `
|
|
2735
|
+
+ `CLI would add ${firebase.version}; add a version in that range to the game `
|
|
2736
|
+
+ `(pnpm add firebase@"${firebaseRange}") or upgrade the CLI, then rerun.`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
const gamePackages = Object.fromEntries(Object.keys(notificationsGameDependencies).map((name) => [name, versionOf(name).version]));
|
|
2740
|
+
const register = 'register createStarterCapacitorNotifications (src/platform/capacitorNotifications.ts) '
|
|
2741
|
+
+ 'or createCapacitorNotificationServiceProvider from @mpgd/adapter-capacitor/notifications in its '
|
|
2742
|
+
+ 'providers';
|
|
2743
|
+
const files = [];
|
|
2744
|
+
const overlayRoot = input.templateRoot === undefined
|
|
2745
|
+
? undefined
|
|
2746
|
+
: path.join(path.dirname(input.templateRoot), 'notifications-overlay', 'common');
|
|
2747
|
+
const jsonTitle = JSON.stringify(input.displayName).slice(1, -1);
|
|
2748
|
+
const starterFile = (root, file) => {
|
|
2749
|
+
// A function replacer: `$&` or `$$` in a display name stays literal.
|
|
2750
|
+
const title = file.endsWith('.json') ? jsonTitle : input.displayName;
|
|
2751
|
+
return readTemplateFile(root, file).replaceAll('__GAME_TITLE__', () => title);
|
|
2752
|
+
};
|
|
2753
|
+
// The wired gateway imports these starter modules; a game-owned copy that
|
|
2754
|
+
// differs keeps the gateway as it is, as a differing store module does.
|
|
2755
|
+
const keptModule = overlayRoot === undefined
|
|
2756
|
+
? undefined
|
|
2757
|
+
: notificationsCapacitorGatewayModules.find((file) => {
|
|
2758
|
+
const existing = readExisting(safeDestination(gameRoot, file));
|
|
2759
|
+
return existing !== undefined
|
|
2760
|
+
&& existing.replace(/\r\n/gu, '\n') !== starterFile(overlayRoot, file);
|
|
2761
|
+
});
|
|
2762
|
+
for (const gatewayFile of Object.values(notificationsCapacitorGateways)) {
|
|
2763
|
+
const current = input.plannedGateways.find((file) => file.path === gatewayFile)?.content
|
|
2764
|
+
?? readExisting(safeDestination(gameRoot, gatewayFile));
|
|
2765
|
+
if (current !== undefined && capacitorGatewayRegistersNotifications(current)) {
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
if (current === undefined) {
|
|
2769
|
+
warnings.push(`${gatewayFile} is missing; ${register}.`);
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
const template = input.templateRoot === undefined
|
|
2773
|
+
? undefined
|
|
2774
|
+
: readTemplateFile(input.templateRoot, gatewayFile);
|
|
2775
|
+
if (template === undefined || current.replace(/\r\n/gu, '\n') !== template) {
|
|
2776
|
+
warnings.push(`${gatewayFile} does not register notifications and is game-owned; ${register}.`);
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
if (keptModule !== undefined) {
|
|
2780
|
+
warnings.push(`${gatewayFile} was not rewritten because the game-owned ${keptModule} differs from the `
|
|
2781
|
+
+ `notifications starter; ${register}.`);
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
files.push({
|
|
2785
|
+
path: gatewayFile,
|
|
2786
|
+
content: wireCapacitorGatewayNotifications(gatewayFile, template),
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
if (files.length > 0 && overlayRoot !== undefined) {
|
|
2790
|
+
// The starter modules and catalog the wired gateways read; the game keeps any it has.
|
|
2791
|
+
for (const file of notificationsCapacitorSupportFiles) {
|
|
2792
|
+
if (readExisting(safeDestination(gameRoot, file)) === undefined) {
|
|
2793
|
+
files.push({ path: file, content: starterFile(overlayRoot, file) });
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
warnings.push('The notification settings screen is not added to an existing game; render one from '
|
|
2797
|
+
+ 'src/platform/notificationSettings.ts (see docs/NOTIFICATIONS.md) so players can turn '
|
|
2798
|
+
+ 'reminders and advertising consent on and off.');
|
|
2799
|
+
}
|
|
2800
|
+
const androidManifest = readExisting(safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`));
|
|
2801
|
+
if (androidManifest !== undefined && !androidManifestRemovesExactAlarm(androidManifest)) {
|
|
2802
|
+
warnings.push(`${shellPath}/android/app/src/main/AndroidManifest.xml keeps SCHEDULE_EXACT_ALARM, which `
|
|
2803
|
+
+ '@capacitor/local-notifications adds; reminders are inexact, so add '
|
|
2804
|
+
+ `${androidInexactAlarmPermission} to the manifest.`);
|
|
2805
|
+
}
|
|
2806
|
+
if (readExisting(safeDestination(gameRoot, `${shellPath}/android/app/google-services.json`)) === undefined) {
|
|
2807
|
+
warnings.push(`Android push needs ${shellPath}/android/app/google-services.json from the Firebase console `
|
|
2808
|
+
+ '(see the mpgd-kit Firebase guide); without it the push half fails closed.');
|
|
2809
|
+
}
|
|
2810
|
+
if (readExisting(safeDestination(gameRoot, `${shellPath}/ios/App/App/GoogleService-Info.plist`))
|
|
2811
|
+
=== undefined) {
|
|
2812
|
+
warnings.push(`iOS push needs ${shellPath}/ios/App/App/GoogleService-Info.plist, the Push Notifications `
|
|
2813
|
+
+ 'capability, an APNs key uploaded to Firebase, and the AppDelegate forwarding the '
|
|
2814
|
+
+ 'messaging plugin documents (see the mpgd-kit Firebase guide).');
|
|
2815
|
+
}
|
|
2816
|
+
return {
|
|
2817
|
+
shellPlugins: shellPlugins.map(({ packageName, version }) => ({ packageName, version })),
|
|
2818
|
+
gameDependencies: gamePackages,
|
|
2819
|
+
files,
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Refuses a package whose declared peers the game's installed copies do not
|
|
2824
|
+
* satisfy (Capacitor core, the kit packages), before anything is written.
|
|
2825
|
+
*/
|
|
2826
|
+
function assertInstalledPeers(gameRoot, label, peerDependencies) {
|
|
2827
|
+
if (typeof peerDependencies !== 'object' || peerDependencies === null) {
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
for (const [peer, range] of Object.entries(peerDependencies)) {
|
|
2831
|
+
const peerFile = path.join(gameRoot, 'node_modules', peer, 'package.json');
|
|
2832
|
+
// A range semver cannot read (`workspace:*`, a tag) is not checked.
|
|
2833
|
+
if (typeof range !== 'string' || validRange(range) === null || !existsSync(peerFile)) {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
const peerVersion = readJsonObject(peerFile).version;
|
|
2837
|
+
if (typeof peerVersion === 'string' && valid(peerVersion) !== null
|
|
2838
|
+
&& !satisfies(peerVersion, range, { includePrerelease: true })) {
|
|
2839
|
+
throw new Error(`${label} needs ${peer} ${range}, but the game has ${peerVersion}; align the game's `
|
|
2840
|
+
+ 'versions before rerunning.');
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2475
2844
|
/** A regular file of the CLI's game template, read without leaving the template root. */
|
|
2476
2845
|
function readTemplateFile(templateRoot, relative) {
|
|
2477
2846
|
const root = realpathSync(templateRoot);
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, verifyHost
|
|
|
24
24
|
import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
|
|
25
25
|
import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
|
|
26
26
|
import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
|
|
27
|
+
import { applyNotificationsStarter, notificationsProviderId, readGameCreateProviders, } from './notifications-starter.js';
|
|
27
28
|
import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
|
|
28
29
|
import { applyToolingMigration, planToolingMigration } from './tooling-migration.js';
|
|
29
30
|
import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
|
|
@@ -58,6 +59,7 @@ const packageRoot = resolvePackageRoot(sourceDir);
|
|
|
58
59
|
const detectedKitRoot = resolveDefaultKitRoot();
|
|
59
60
|
const gameTemplateDir = path.resolve(packageRoot, 'templates/phaser-game');
|
|
60
61
|
const liteTemplateOverlayDir = path.resolve(packageRoot, 'templates/lite-overlay');
|
|
62
|
+
const notificationsTemplateDir = path.resolve(packageRoot, 'templates/notifications-overlay');
|
|
61
63
|
const supportedGameTemplates = ['phaser', 'lite'];
|
|
62
64
|
/** Lite renderers implemented today. dom/svg are reserved extension points in @mpgd/lite-shell. */
|
|
63
65
|
const supportedLiteRenderers = ['canvas'];
|
|
@@ -575,6 +577,10 @@ const gameCommand = defineI18n({
|
|
|
575
577
|
en: 'Lite template renderer. Only canvas is implemented today.',
|
|
576
578
|
ko: 'lite 템플릿 렌더러. 현재는 canvas만 지원합니다.',
|
|
577
579
|
},
|
|
580
|
+
providers: {
|
|
581
|
+
en: 'Comma-separated game providers. notifications adds the notifications v2 starter: the topic catalog, the backend settings, the Android, iOS, Apps in Toss, and Devvit wiring, and a notification settings screen.',
|
|
582
|
+
ko: '쉼표로 구분한 게임 provider. notifications는 알림 v2 스타터(토픽 카탈로그, 백엔드 설정, Android·iOS·앱인토스·Devvit 연결, 알림 설정 화면)를 추가합니다.',
|
|
583
|
+
},
|
|
578
584
|
}),
|
|
579
585
|
args: {
|
|
580
586
|
template: {
|
|
@@ -627,6 +633,11 @@ const gameCommand = defineI18n({
|
|
|
627
633
|
required: false,
|
|
628
634
|
description: 'Include the Microsoft Store PWA target and release workflow.',
|
|
629
635
|
},
|
|
636
|
+
providers: {
|
|
637
|
+
type: 'string',
|
|
638
|
+
required: false,
|
|
639
|
+
description: 'Comma-separated game providers. notifications adds the notifications v2 starter.',
|
|
640
|
+
},
|
|
630
641
|
},
|
|
631
642
|
run: (ctx) => {
|
|
632
643
|
const positionals = readLocalPositionals(ctx.positionals, ['game', 'create']);
|
|
@@ -649,6 +660,7 @@ const gameCommand = defineI18n({
|
|
|
649
660
|
...(kitPath === undefined ? {} : { kitPath }),
|
|
650
661
|
dryRun: ctx.values['dry-run'] === true,
|
|
651
662
|
microsoftStore: ctx.values['microsoft-store'] === true,
|
|
663
|
+
providers: readGameCreateProviders(readOptionalString(ctx.values.providers)),
|
|
652
664
|
});
|
|
653
665
|
},
|
|
654
666
|
}),
|
|
@@ -1597,8 +1609,8 @@ const targetCommand = defineI18n({
|
|
|
1597
1609
|
ko: '프로덕션 HTTPS Game Services 기본 URL.',
|
|
1598
1610
|
},
|
|
1599
1611
|
providers: {
|
|
1600
|
-
en: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; other IDs are recorded only.',
|
|
1601
|
-
ko: '쉼표로 구분한 provider ID. play-billing(Android)과 storekit(iOS)은 키트 스토어 플러그인을 셸과 게임에 설치하고 cap sync로 등록하며, 연결되지 않은 템플릿 게이트웨이를 연결합니다. 다른 ID는 기록만 합니다.',
|
|
1612
|
+
en: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; other IDs are recorded only.',
|
|
1613
|
+
ko: '쉼표로 구분한 provider ID. play-billing(Android)과 storekit(iOS)은 키트 스토어 플러그인을 셸과 게임에 설치하고 cap sync로 등록하며, 연결되지 않은 템플릿 게이트웨이를 연결합니다. notifications는 Android·iOS용 Firebase 메시징과 로컬 알림 플러그인을 설치하고 템플릿 게이트웨이를 연결합니다. 다른 ID는 기록만 합니다.',
|
|
1602
1614
|
},
|
|
1603
1615
|
}),
|
|
1604
1616
|
args: {
|
|
@@ -1646,7 +1658,7 @@ const targetCommand = defineI18n({
|
|
|
1646
1658
|
providers: {
|
|
1647
1659
|
type: 'string',
|
|
1648
1660
|
required: false,
|
|
1649
|
-
description: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; other IDs are recorded only.',
|
|
1661
|
+
description: 'Comma-separated provider IDs. play-billing (Android) and storekit (iOS) install the kit store plugin into the shell and the game, register it with cap sync, and wire an unwired template gateway; notifications installs the Firebase messaging and local notifications plugins for Android and iOS and wires a template gateway; other IDs are recorded only.',
|
|
1650
1662
|
},
|
|
1651
1663
|
},
|
|
1652
1664
|
run: (ctx) => {
|
|
@@ -1693,6 +1705,11 @@ const targetCommand = defineI18n({
|
|
|
1693
1705
|
+ `${plugin.versionSource === 'kit' ? ' and the game' : ''}`
|
|
1694
1706
|
+ `${dryRun ? '; cap add or cap sync registers it' : ''}.`);
|
|
1695
1707
|
}
|
|
1708
|
+
if (plan.notificationPlugins.length > 0) {
|
|
1709
|
+
console.info(`${dryRun ? 'Would install' : 'Installed'} notifications plugins `
|
|
1710
|
+
+ plan.notificationPlugins.map((plugin) => `${plugin.packageName}@${plugin.version}`).join(', ')
|
|
1711
|
+
+ ` into the shell for android and ios${dryRun ? '; cap add or cap sync registers them' : ''}.`);
|
|
1712
|
+
}
|
|
1696
1713
|
for (const warning of plan.warnings) {
|
|
1697
1714
|
console.warn(`Warning: ${warning}`);
|
|
1698
1715
|
}
|
|
@@ -2943,9 +2960,17 @@ function createGameApp(input) {
|
|
|
2943
2960
|
microsoftStore: input.microsoftStore,
|
|
2944
2961
|
});
|
|
2945
2962
|
const baseTemplateFiles = collectTemplateFiles(gameTemplateDir);
|
|
2946
|
-
const
|
|
2963
|
+
const templateFiles = input.template === 'lite'
|
|
2947
2964
|
? applyLiteTemplateOverlay(baseTemplateFiles, collectTemplateFiles(liteTemplateOverlayDir))
|
|
2948
2965
|
: baseTemplateFiles;
|
|
2966
|
+
const selectedTemplateFiles = input.providers?.includes(notificationsProviderId) === true
|
|
2967
|
+
? applyNotificationsStarter(templateFiles, {
|
|
2968
|
+
template: input.template === 'lite' ? 'lite' : 'phaser',
|
|
2969
|
+
common: collectTemplateFiles(path.join(notificationsTemplateDir, 'common')),
|
|
2970
|
+
phaser: collectTemplateFiles(path.join(notificationsTemplateDir, 'phaser')),
|
|
2971
|
+
lite: collectTemplateFiles(path.join(notificationsTemplateDir, 'lite')),
|
|
2972
|
+
})
|
|
2973
|
+
: templateFiles;
|
|
2949
2974
|
const files = selectedTemplateFiles.flatMap((file) => {
|
|
2950
2975
|
const prepared = prepareBaseGameTemplateFile({
|
|
2951
2976
|
relativePath: file.relativePath,
|
|
@@ -3371,8 +3396,14 @@ function createGameOwnedReleaseEnv(gameRoot, baseEnv) {
|
|
|
3371
3396
|
?? readSourceGitSha(gameRoot);
|
|
3372
3397
|
const appVersion = readOptionalString(baseEnv.APP_VERSION)
|
|
3373
3398
|
?? readGamePackageVersion(gameRoot);
|
|
3399
|
+
const notificationTopicsFile = readConfiguredPath(baseEnv.MPGD_NOTIFICATION_TOPICS_FILE);
|
|
3374
3400
|
return {
|
|
3375
3401
|
...monetizationFiles,
|
|
3402
|
+
// The Apps in Toss wrapper builds in its own directory; a relative path
|
|
3403
|
+
// names a file of the game, as `pnpm validate:notifications` reads it.
|
|
3404
|
+
...(notificationTopicsFile === undefined
|
|
3405
|
+
? {}
|
|
3406
|
+
: { MPGD_NOTIFICATION_TOPICS_FILE: path.resolve(gameRoot, notificationTopicsFile) }),
|
|
3376
3407
|
MPGD_SOURCE_GIT_SHA: sourceGitSha,
|
|
3377
3408
|
...(appVersion === undefined ? {} : { APP_VERSION: appVersion }),
|
|
3378
3409
|
};
|