@mpgd/cli 0.52.0 → 0.54.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 +395 -18
- package/dist/deploy-planning.js +2 -2
- package/dist/index.js +122 -34
- package/dist/kit-upgrade.js +2 -2
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +433 -2
- package/dist/notifications-starter.d.ts +75 -0
- package/dist/notifications-starter.js +522 -0
- package/dist/offline-playtest.js +1 -1
- package/dist/tooling-migration.js +1 -1
- package/package.json +16 -16
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +104 -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/notificationBackend.ts +157 -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 +163 -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 +301 -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
- package/templates/phaser-game/src/platform/gameServices.ts +3 -0
- package/templates/phaser-game/src/platform/serverSession.ts +66 -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, notificationsCapacitorTemplateSupportFiles, 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,295 @@ 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
|
+
// Base-template modules they import, missing from a game an earlier release created.
|
|
2797
|
+
if (input.templateRoot !== undefined) {
|
|
2798
|
+
for (const file of notificationsCapacitorTemplateSupportFiles) {
|
|
2799
|
+
if (readExisting(safeDestination(gameRoot, file)) === undefined) {
|
|
2800
|
+
files.push({ path: file, content: starterFile(input.templateRoot, file) });
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
warnings.push('The notification settings screen is not added to an existing game; render one from '
|
|
2805
|
+
+ 'src/platform/notificationSettings.ts (see docs/NOTIFICATIONS.md) so players can turn '
|
|
2806
|
+
+ 'reminders and advertising consent on and off.');
|
|
2807
|
+
}
|
|
2808
|
+
const androidManifest = readExisting(safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`));
|
|
2809
|
+
if (androidManifest !== undefined && !androidManifestRemovesExactAlarm(androidManifest)) {
|
|
2810
|
+
warnings.push(`${shellPath}/android/app/src/main/AndroidManifest.xml keeps SCHEDULE_EXACT_ALARM, which `
|
|
2811
|
+
+ '@capacitor/local-notifications adds; reminders are inexact, so add '
|
|
2812
|
+
+ `${androidInexactAlarmPermission} to the manifest.`);
|
|
2813
|
+
}
|
|
2814
|
+
if (readExisting(safeDestination(gameRoot, `${shellPath}/android/app/google-services.json`)) === undefined) {
|
|
2815
|
+
warnings.push(`Android push needs ${shellPath}/android/app/google-services.json from the Firebase console `
|
|
2816
|
+
+ '(see the mpgd-kit Firebase guide); without it the push half fails closed.');
|
|
2817
|
+
}
|
|
2818
|
+
if (readExisting(safeDestination(gameRoot, `${shellPath}/ios/App/App/GoogleService-Info.plist`))
|
|
2819
|
+
=== undefined) {
|
|
2820
|
+
warnings.push(`iOS push needs ${shellPath}/ios/App/App/GoogleService-Info.plist, the Push Notifications `
|
|
2821
|
+
+ 'capability, an APNs key uploaded to Firebase, and the AppDelegate forwarding the '
|
|
2822
|
+
+ 'messaging plugin documents (see the mpgd-kit Firebase guide).');
|
|
2823
|
+
}
|
|
2824
|
+
return {
|
|
2825
|
+
shellPlugins: shellPlugins.map(({ packageName, version }) => ({ packageName, version })),
|
|
2826
|
+
gameDependencies: gamePackages,
|
|
2827
|
+
files,
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Refuses a package whose declared peers the game's installed copies do not
|
|
2832
|
+
* satisfy (Capacitor core, the kit packages), before anything is written.
|
|
2833
|
+
*/
|
|
2834
|
+
function assertInstalledPeers(gameRoot, label, peerDependencies) {
|
|
2835
|
+
if (typeof peerDependencies !== 'object' || peerDependencies === null) {
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
for (const [peer, range] of Object.entries(peerDependencies)) {
|
|
2839
|
+
const peerFile = path.join(gameRoot, 'node_modules', peer, 'package.json');
|
|
2840
|
+
// A range semver cannot read (`workspace:*`, a tag) is not checked.
|
|
2841
|
+
if (typeof range !== 'string' || validRange(range) === null || !existsSync(peerFile)) {
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
const peerVersion = readJsonObject(peerFile).version;
|
|
2845
|
+
if (typeof peerVersion === 'string' && valid(peerVersion) !== null
|
|
2846
|
+
&& !satisfies(peerVersion, range, { includePrerelease: true })) {
|
|
2847
|
+
throw new Error(`${label} needs ${peer} ${range}, but the game has ${peerVersion}; align the game's `
|
|
2848
|
+
+ 'versions before rerunning.');
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2475
2852
|
/** A regular file of the CLI's game template, read without leaving the template root. */
|
|
2476
2853
|
function readTemplateFile(templateRoot, relative) {
|
|
2477
2854
|
const root = realpathSync(templateRoot);
|
package/dist/deploy-planning.js
CHANGED
|
@@ -356,8 +356,8 @@ function assertGameOwnedNativePaths(gameRoot, target, config) {
|
|
|
356
356
|
throw new Error(`${target} ${field} still points to a Kit checkout; initialize a game-owned shell.`);
|
|
357
357
|
}
|
|
358
358
|
const expanded = value
|
|
359
|
-
.replaceAll('${MPGD_GAME_ROOT}', root)
|
|
360
|
-
.replaceAll('${MPGD_GAME_APP_ROOT}', root);
|
|
359
|
+
.replaceAll('${MPGD_GAME_ROOT}', () => root)
|
|
360
|
+
.replaceAll('${MPGD_GAME_APP_ROOT}', () => root);
|
|
361
361
|
const candidate = resolve(root, expanded);
|
|
362
362
|
let ancestor = candidate;
|
|
363
363
|
while (!existsSync(ancestor)) {
|