@mpgd/cli 0.57.0 → 0.58.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 +30 -0
- package/dist/capacitor-shell-starter.js +243 -11
- package/dist/index.js +170 -11
- package/dist/legal-site-output.d.ts +18 -0
- package/dist/legal-site-output.js +46 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +1663 -899
- package/dist/native-deploy-run.js +4 -0
- package/dist/native-shell-store-policy.d.ts +123 -0
- package/dist/native-shell-store-policy.js +816 -0
- package/dist/release-state.d.ts +2 -0
- package/dist/release-state.js +4 -2
- package/dist/testflight-submission.d.ts +9 -0
- package/dist/testflight-submission.js +69 -0
- package/package.json +17 -17
- package/templates/lite-overlay/agent/acceptance.md +1 -1
- package/templates/lite-overlay/agent/brief.md +1 -1
- package/templates/lite-overlay/src/lite/deleteAccountButton.ts +173 -0
- package/templates/lite-overlay/src/lite/restorePurchasesButton.ts +63 -0
- package/templates/lite-overlay/src/runtime/createGame.ts +8 -0
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +12 -6
- package/templates/notifications-overlay/common/notifications/deployment.json +2 -1
- package/templates/notifications-overlay/common/notifications/topics.json +3 -0
- package/templates/notifications-overlay/common/src/platform/capacitorNotifications.ts +8 -3
- package/templates/notifications-overlay/common/src/platform/notifications.ts +17 -3
- package/templates/phaser-game/README.md +72 -0
- package/templates/phaser-game/agent/acceptance.md +4 -1
- package/templates/phaser-game/agent/brief.md +14 -4
- package/templates/phaser-game/gitignore +11 -0
- package/templates/phaser-game/legal/delete-account.html +133 -0
- package/templates/phaser-game/legal/privacy.html +111 -12
- package/templates/phaser-game/legal/support.html +1 -0
- package/templates/phaser-game/legal/terms.html +1 -0
- package/templates/phaser-game/mpgd.game.json +3 -0
- package/templates/phaser-game/src/env.d.ts +6 -0
- package/templates/phaser-game/src/i18n/messages.ts +86 -1
- package/templates/phaser-game/src/main.ts +69 -8
- package/templates/phaser-game/src/platform/commerceAccountBinding.ts +39 -10
- package/templates/phaser-game/src/platform/deleteAccount.ts +334 -0
- package/templates/phaser-game/src/platform/gameServices.ts +55 -10
- package/templates/phaser-game/src/platform/guestIdentity.ts +301 -0
- package/templates/phaser-game/src/platform/restorePurchases.ts +205 -0
- package/templates/phaser-game/src/platform/serverSession.ts +8 -0
- package/templates/phaser-game/src/platform/sessionGatedServices.ts +217 -0
- package/templates/phaser-game/src/scenes/LobbyScene.ts +254 -1
- package/templates/phaser-game/src/scenes/PlayScene.ts +16 -6
|
@@ -137,4 +137,34 @@ export declare function androidManifestRemovesExactAlarm(manifest: string): bool
|
|
|
137
137
|
* returned unchanged.
|
|
138
138
|
*/
|
|
139
139
|
export declare function applyAndroidManifestInexactAlarms(manifest: string): string;
|
|
140
|
+
/** The kit's messaging service that renders Android advertising pushes with the opt-out action. */
|
|
141
|
+
export declare const androidAdvertisingRendererService = "dev.mpgd.capacitor.AdvertisingMessagingService";
|
|
142
|
+
/** The messaging service `@capacitor-firebase/messaging` 8.x declares, which the renderer replaces. */
|
|
143
|
+
export declare const androidMessagingPluginService = "io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService";
|
|
144
|
+
/** The meta-data the renderer's service declares, which `getAdvertisingPushRenderer` reads. */
|
|
145
|
+
export declare const androidAdvertisingRendererMetaData = "dev.mpgd.notifications.advertising_renderer";
|
|
146
|
+
/** The renderer's service element, then the removal of the plugin's service. */
|
|
147
|
+
export declare const androidAdvertisingRendererEntries: readonly string[];
|
|
148
|
+
/**
|
|
149
|
+
* Whether an Android manifest declares the kit's advertising renderer as its
|
|
150
|
+
* messaging service, with the messaging event and its meta-data, and removes
|
|
151
|
+
* the plugin's service. Comments do not count.
|
|
152
|
+
*/
|
|
153
|
+
export declare function androidManifestRendersAdvertising(manifest: string): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Declares the kit's advertising renderer as the shell's messaging service
|
|
156
|
+
* inside `<application>` (`androidAdvertisingRendererEntries`), binding the
|
|
157
|
+
* tools namespace on the manifest root when missing. Each part is added only
|
|
158
|
+
* when it is missing, so a manifest that has one keeps it once; a renderer
|
|
159
|
+
* service that is declared but incomplete is left for the game to fix (the
|
|
160
|
+
* initializer warns). A manifest this cannot place it in (no `<application>`
|
|
161
|
+
* with a closing tag) is returned unchanged.
|
|
162
|
+
*/
|
|
163
|
+
export declare function applyAndroidManifestAdvertisingRenderer(manifest: string): string;
|
|
164
|
+
/**
|
|
165
|
+
* The initializer's warning for a game-owned Android manifest without the
|
|
166
|
+
* kit's advertising renderer: the manifest lines, the provider wiring, and
|
|
167
|
+
* the deployment switch. `undefined` when the manifest has the renderer.
|
|
168
|
+
*/
|
|
169
|
+
export declare function androidAdvertisingRendererWarning(manifest: string): string | undefined;
|
|
140
170
|
export {};
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { DOMParser } from '@xmldom/xmldom';
|
|
7
7
|
import { satisfies, valid, validRange } from 'semver';
|
|
8
8
|
import { capacitorGatewayRegistersNotifications, conflictingPushPlugin, notificationsCapacitorGatewayModules, notificationsCapacitorGateways, notificationsCapacitorSupportFiles, notificationsCapacitorTemplateSupportFiles, notificationsCapacitorTemplateSupportImporters, notificationsGameDependencies, notificationsMessagingFirebaseRange, notificationsMessagingPlugin, notificationsProviderId, notificationsShellPlugins, sourceReferencesRelativeModule, wireCapacitorGatewayNotifications, } from './notifications-starter.js';
|
|
9
|
+
import { addIosAppResource, applyAndroidManifestAdId, applyAndroidManifestOrientation, applyIosInfoPlistOrientation, applyIosInfoPlistStorePolicy, applyIosProjectDeviceFamily, iosAppPrivacyManifestFile, iosAppPrivacyManifestXml, isRecord, readGameAdvertisingIdPolicy, readGameNativeOrientation, readIosDeviceFamily, shellUsesAdMob, } from './native-shell-store-policy.js';
|
|
9
10
|
import { isNonPublicServiceHostname } from './production-target-readiness.js';
|
|
10
11
|
import { assertAndroidSettingsAppProject, assertAndroidSettingsNoAppRemap, assertIosReleaseInfoPlistExpansion, assertIosReleasePackagingSettings, assertIosReleaseProductName, assertNativeShellIdentity, hasAndroidDisplayNameResourceOverride, hasAndroidManifestSourceSetOverride, hasAndroidResourceSourceSetOverride, hasGradleIdentityMutation, hasGradleTaskAction, maskGradleStrings, readIosAppTargetId, readIosReleaseInfoPlist, stripGradleComments, } from './native-shell-identity.js';
|
|
11
12
|
const shellPath = 'apps/mobile-capacitor';
|
|
@@ -153,6 +154,9 @@ export function planCapacitorShellStarter(input) {
|
|
|
153
154
|
const cliDevelopmentDependencies = requireObject(cliPackage.devDependencies, 'CLI dev dependencies');
|
|
154
155
|
const typescriptVersion = requireString(cliDevelopmentDependencies.typescript, 'CLI TypeScript version');
|
|
155
156
|
const targetMap = requireObject(targets.targets, 'targets');
|
|
157
|
+
// Fail before anything is installed or generated when the store-facing
|
|
158
|
+
// native settings are invalid; they are applied after `cap add`.
|
|
159
|
+
readNativeShellSettings(gameRoot);
|
|
156
160
|
for (const plugin of capacitorShellProviderPlugins) {
|
|
157
161
|
if (providerIds.includes(plugin.id) && targetMap[plugin.platform] === undefined) {
|
|
158
162
|
throw new Error(`--providers ${plugin.id} installs the ${plugin.platform} store plugin into the native `
|
|
@@ -653,7 +657,12 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
|
|
|
653
657
|
materializeAndroidBackupPolicy(plan.gameRoot);
|
|
654
658
|
if (plan.notificationPlugins.length > 0) {
|
|
655
659
|
materializeAndroidInexactAlarms(plan.gameRoot);
|
|
660
|
+
materializeAndroidAdvertisingRenderer(plan.gameRoot);
|
|
656
661
|
}
|
|
662
|
+
materializeAndroidStorePolicy(plan.gameRoot);
|
|
663
|
+
}
|
|
664
|
+
if (platform === 'ios' && generatedNow) {
|
|
665
|
+
materializeIosStorePolicy(plan.gameRoot);
|
|
657
666
|
}
|
|
658
667
|
if (platform === 'ios') {
|
|
659
668
|
materializeIosArchiveScheme(plan.gameRoot);
|
|
@@ -800,6 +809,89 @@ export function applyAndroidManifestBackupPolicy(manifest) {
|
|
|
800
809
|
applicationTag = applicationTag.replace(/^<application\b/u, `<application${attributes}`);
|
|
801
810
|
return `${manifest.slice(0, root.index)}${rootTag}${manifest.slice(root.index + root[0].length, application.index)}${applicationTag}${manifest.slice(application.index + application[0].length)}`;
|
|
802
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* Store-facing Info.plist keys for a freshly generated iOS project only
|
|
814
|
+
* (`applyIosInfoPlistStorePolicy`); an existing game-owned project is left
|
|
815
|
+
* alone and the production build reports what it lacks.
|
|
816
|
+
*/
|
|
817
|
+
function materializeIosStorePolicy(gameRoot) {
|
|
818
|
+
const settings = readNativeShellSettings(gameRoot);
|
|
819
|
+
const plistFile = safeDestination(gameRoot, `${shellPath}/ios/App/App/Info.plist`);
|
|
820
|
+
if (existsSync(plistFile) && lstatSync(plistFile).isFile()) {
|
|
821
|
+
const source = readFileSync(plistFile, 'utf8');
|
|
822
|
+
let next = applyIosInfoPlistStorePolicy(source);
|
|
823
|
+
if (settings.orientation !== undefined) {
|
|
824
|
+
next = applyIosInfoPlistOrientation(next, settings.orientation);
|
|
825
|
+
}
|
|
826
|
+
if (next !== source) {
|
|
827
|
+
writeFileSync(plistFile, next);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const projectFile = safeDestination(gameRoot, `${shellPath}/ios/App/App.xcodeproj/project.pbxproj`);
|
|
831
|
+
const source = readFileSync(projectFile, 'utf8');
|
|
832
|
+
let next = settings.iosDeviceFamily === undefined
|
|
833
|
+
? source
|
|
834
|
+
: applyIosProjectDeviceFamily(source, settings.iosDeviceFamily);
|
|
835
|
+
// The app-level privacy manifest joins the App group and Resources phase of
|
|
836
|
+
// the Capacitor-generated layout; any other layout is left to the game and
|
|
837
|
+
// reported by the build.
|
|
838
|
+
const manifestFile = safeDestination(gameRoot, `${shellPath}/ios/App/App/${iosAppPrivacyManifestFile}`);
|
|
839
|
+
const withManifest = addIosAppResource(next, iosAppPrivacyManifestFile);
|
|
840
|
+
if (withManifest === undefined) {
|
|
841
|
+
console.warn(`[mpgd] The generated iOS project has an unexpected layout; add ${iosAppPrivacyManifestFile} `
|
|
842
|
+
+ 'to the App target yourself.');
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
if (!existsSync(manifestFile)) {
|
|
846
|
+
writeFileSync(manifestFile, iosAppPrivacyManifestXml, { flag: 'wx' });
|
|
847
|
+
}
|
|
848
|
+
next = withManifest;
|
|
849
|
+
}
|
|
850
|
+
if (next !== source) {
|
|
851
|
+
writeFileSync(projectFile, next);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Orientation of a freshly generated Android project's launcher activity,
|
|
856
|
+
* `android:appCategory="game"`, and the advertising ID permission (declared
|
|
857
|
+
* when the shell installs AdMob and mpgd.game.json does not set
|
|
858
|
+
* `ads.admob.advertisingId` to "removed", removed otherwise); an existing
|
|
859
|
+
* project is only reported by the production build.
|
|
860
|
+
*/
|
|
861
|
+
function materializeAndroidStorePolicy(gameRoot) {
|
|
862
|
+
const manifestFile = safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`);
|
|
863
|
+
if (!existsSync(manifestFile) || !lstatSync(manifestFile).isFile()) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const source = readFileSync(manifestFile, 'utf8');
|
|
867
|
+
const settings = readNativeShellSettings(gameRoot);
|
|
868
|
+
const shellPackageFile = path.join(gameRoot, shellPath, 'package.json');
|
|
869
|
+
const usesAds = existsSync(shellPackageFile) && shellUsesAdMob(readJsonObject(shellPackageFile));
|
|
870
|
+
const next = applyAndroidManifestAdId(applyAndroidManifestOrientation(source, settings.orientation ?? 'any'), usesAds && settings.advertisingId !== 'removed');
|
|
871
|
+
if (next !== source) {
|
|
872
|
+
writeFileSync(manifestFile, next);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* The game's design orientation and advertising ID policy (mpgd.game.json)
|
|
877
|
+
* and iOS device family (ios target metadata).
|
|
878
|
+
*/
|
|
879
|
+
function readNativeShellSettings(gameRoot) {
|
|
880
|
+
const gameConfigFile = path.join(gameRoot, 'mpgd.game.json');
|
|
881
|
+
const gameConfig = existsSync(gameConfigFile)
|
|
882
|
+
? parseJsonObject(readFileSync(gameConfigFile, 'utf8'), 'mpgd.game.json')
|
|
883
|
+
: undefined;
|
|
884
|
+
const orientation = readGameNativeOrientation(gameConfig);
|
|
885
|
+
const advertisingId = readGameAdvertisingIdPolicy(gameConfig);
|
|
886
|
+
const targets = readJsonObject(path.join(gameRoot, 'mpgd.targets.json'));
|
|
887
|
+
const ios = isRecord(targets.targets) ? targets.targets.ios : undefined;
|
|
888
|
+
const iosDeviceFamily = readIosDeviceFamily(isRecord(ios) ? ios.metadata : undefined);
|
|
889
|
+
return {
|
|
890
|
+
...(orientation === undefined ? {} : { orientation }),
|
|
891
|
+
...(iosDeviceFamily === undefined ? {} : { iosDeviceFamily }),
|
|
892
|
+
advertisingId,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
803
895
|
function materializeIosArchiveScheme(gameRoot) {
|
|
804
896
|
const schemeRelative = `${shellPath}/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme`;
|
|
805
897
|
const schemeFile = safeDestination(gameRoot, schemeRelative);
|
|
@@ -2600,6 +2692,132 @@ export function applyAndroidManifestInexactAlarms(manifest) {
|
|
|
2600
2692
|
const head = `${manifest.slice(0, root.index)}${rootTag}${manifest.slice(root.index + root[0].length, close)}`;
|
|
2601
2693
|
return `${head.trimEnd()}${newline} ${androidInexactAlarmPermission}${newline}${manifest.slice(close)}`;
|
|
2602
2694
|
}
|
|
2695
|
+
/** The kit's messaging service that renders Android advertising pushes with the opt-out action. */
|
|
2696
|
+
export const androidAdvertisingRendererService = 'dev.mpgd.capacitor.AdvertisingMessagingService';
|
|
2697
|
+
/** The messaging service `@capacitor-firebase/messaging` 8.x declares, which the renderer replaces. */
|
|
2698
|
+
export const androidMessagingPluginService = 'io.capawesome.capacitorjs.plugins.firebase.messaging.MessagingService';
|
|
2699
|
+
/** The meta-data the renderer's service declares, which `getAdvertisingPushRenderer` reads. */
|
|
2700
|
+
export const androidAdvertisingRendererMetaData = 'dev.mpgd.notifications.advertising_renderer';
|
|
2701
|
+
/**
|
|
2702
|
+
* The manifest entries that make the kit's renderer the shell's one Firebase
|
|
2703
|
+
* messaging service (notifications RFC "Capacitor"). Android hands a
|
|
2704
|
+
* messaging event to one service, so the plugin's own is removed from the
|
|
2705
|
+
* merge; the kit's forwards every message it does not render, and every new
|
|
2706
|
+
* token, to the plugin.
|
|
2707
|
+
*/
|
|
2708
|
+
const androidAdvertisingRendererServiceEntry = [
|
|
2709
|
+
`<service android:name="${androidAdvertisingRendererService}" android:exported="false">`,
|
|
2710
|
+
' <intent-filter>',
|
|
2711
|
+
' <action android:name="com.google.firebase.MESSAGING_EVENT" />',
|
|
2712
|
+
' </intent-filter>',
|
|
2713
|
+
` <meta-data android:name="${androidAdvertisingRendererMetaData}" android:value="true" />`,
|
|
2714
|
+
'</service>',
|
|
2715
|
+
];
|
|
2716
|
+
const androidMessagingPluginServiceRemoval = `<service android:name="${androidMessagingPluginService}" tools:node="remove" />`;
|
|
2717
|
+
/** The renderer's service element, then the removal of the plugin's service. */
|
|
2718
|
+
export const androidAdvertisingRendererEntries = [
|
|
2719
|
+
...androidAdvertisingRendererServiceEntry,
|
|
2720
|
+
androidMessagingPluginServiceRemoval,
|
|
2721
|
+
];
|
|
2722
|
+
/** `text` as a literal inside a regular expression. */
|
|
2723
|
+
function regexLiteral(text) {
|
|
2724
|
+
// Only syntax characters: the `u` flag refuses an escaped `-` or letter.
|
|
2725
|
+
return text.replace(/[.*+?^${}()|[\]\\/]/gu, '\\$&');
|
|
2726
|
+
}
|
|
2727
|
+
/** An attribute `name="value"` (or `'value'`) inside a start tag; both are literal. */
|
|
2728
|
+
function xmlAttributeLookahead(name, value) {
|
|
2729
|
+
const attribute = `\\s${regexLiteral(name)}\\s*=\\s*`;
|
|
2730
|
+
const literal = regexLiteral(value);
|
|
2731
|
+
return `(?=[^>]*${attribute}(?:"${literal}"|'${literal}'))`;
|
|
2732
|
+
}
|
|
2733
|
+
/** The manifest's `<service>` element for `name`, self-closed or with a body, or `undefined`. */
|
|
2734
|
+
function androidServiceElement(code, name) {
|
|
2735
|
+
return new RegExp(`<service\\b${xmlAttributeLookahead('android:name', name)}[^>]*?(?:/>|>[\\s\\S]*?</service>)`, 'u').exec(code)?.[0];
|
|
2736
|
+
}
|
|
2737
|
+
/** Whether the manifest removes the messaging plugin's own service from the merge. */
|
|
2738
|
+
function androidManifestRemovesMessagingPluginService(code) {
|
|
2739
|
+
return new RegExp(`<service\\b${xmlAttributeLookahead('android:name', androidMessagingPluginService)}`
|
|
2740
|
+
+ `${xmlAttributeLookahead('tools:node', 'remove')}[^>]*>`, 'u').test(code);
|
|
2741
|
+
}
|
|
2742
|
+
/**
|
|
2743
|
+
* Whether an Android manifest declares the kit's advertising renderer as its
|
|
2744
|
+
* messaging service, with the messaging event and its meta-data, and removes
|
|
2745
|
+
* the plugin's service. Comments do not count.
|
|
2746
|
+
*/
|
|
2747
|
+
export function androidManifestRendersAdvertising(manifest) {
|
|
2748
|
+
const code = withoutXmlComments(manifest);
|
|
2749
|
+
const renderer = androidServiceElement(code, androidAdvertisingRendererService);
|
|
2750
|
+
return renderer !== undefined
|
|
2751
|
+
&& renderer.includes('com.google.firebase.MESSAGING_EVENT')
|
|
2752
|
+
&& new RegExp(`<meta-data\\b${xmlAttributeLookahead('android:name', androidAdvertisingRendererMetaData)}`
|
|
2753
|
+
+ `${xmlAttributeLookahead('android:value', 'true')}`, 'u').test(renderer)
|
|
2754
|
+
&& androidManifestRemovesMessagingPluginService(code);
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Declares the kit's advertising renderer as the shell's messaging service
|
|
2758
|
+
* inside `<application>` (`androidAdvertisingRendererEntries`), binding the
|
|
2759
|
+
* tools namespace on the manifest root when missing. Each part is added only
|
|
2760
|
+
* when it is missing, so a manifest that has one keeps it once; a renderer
|
|
2761
|
+
* service that is declared but incomplete is left for the game to fix (the
|
|
2762
|
+
* initializer warns). A manifest this cannot place it in (no `<application>`
|
|
2763
|
+
* with a closing tag) is returned unchanged.
|
|
2764
|
+
*/
|
|
2765
|
+
export function applyAndroidManifestAdvertisingRenderer(manifest) {
|
|
2766
|
+
if (androidManifestRendersAdvertising(manifest)) {
|
|
2767
|
+
return manifest;
|
|
2768
|
+
}
|
|
2769
|
+
const code = withoutXmlComments(manifest);
|
|
2770
|
+
const missing = [
|
|
2771
|
+
...(androidServiceElement(code, androidAdvertisingRendererService) === undefined
|
|
2772
|
+
? androidAdvertisingRendererServiceEntry
|
|
2773
|
+
: []),
|
|
2774
|
+
...(androidManifestRemovesMessagingPluginService(code) ? [] : [androidMessagingPluginServiceRemoval]),
|
|
2775
|
+
];
|
|
2776
|
+
const root = /<manifest\b[^>]*?>/u.exec(code);
|
|
2777
|
+
const close = code.lastIndexOf('</application>');
|
|
2778
|
+
const application = /<application\b[^>]*?>/u.exec(code);
|
|
2779
|
+
if (missing.length === 0 || root === null || application === null || close === -1
|
|
2780
|
+
|| root[0].endsWith('/>') || application[0].endsWith('/>') || close < application.index
|
|
2781
|
+
|| application.index < root.index) {
|
|
2782
|
+
return manifest;
|
|
2783
|
+
}
|
|
2784
|
+
const newline = manifest.includes('\r\n') ? '\r\n' : '\n';
|
|
2785
|
+
let rootTag = manifest.slice(root.index, root.index + root[0].length);
|
|
2786
|
+
if (!/\sxmlns:tools\s*=/u.test(rootTag)) {
|
|
2787
|
+
rootTag = rootTag.replace(/^<manifest\b/u, `<manifest xmlns:tools="${androidToolsNamespace}"`);
|
|
2788
|
+
}
|
|
2789
|
+
const head = `${manifest.slice(0, root.index)}${rootTag}${manifest.slice(root.index + root[0].length, close)}`;
|
|
2790
|
+
const entries = missing.map((line) => `${newline} ${line}`).join('');
|
|
2791
|
+
return `${head.trimEnd()}${entries}${newline} ${manifest.slice(close)}`;
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* The initializer's warning for a game-owned Android manifest without the
|
|
2795
|
+
* kit's advertising renderer: the manifest lines, the provider wiring, and
|
|
2796
|
+
* the deployment switch. `undefined` when the manifest has the renderer.
|
|
2797
|
+
*/
|
|
2798
|
+
export function androidAdvertisingRendererWarning(manifest) {
|
|
2799
|
+
if (androidManifestRendersAdvertising(manifest)) {
|
|
2800
|
+
return undefined;
|
|
2801
|
+
}
|
|
2802
|
+
return `${shellPath}/android/app/src/main/AndroidManifest.xml does not declare the kit's advertising `
|
|
2803
|
+
+ 'renderer, so under the Korean rules this shell receives no advertising push (the backend '
|
|
2804
|
+
+ 'sends Android advertising only to a shell that shows the opt-out action). To render them, '
|
|
2805
|
+
+ `add inside <application> (with xmlns:tools on <manifest>): ${androidAdvertisingRendererEntries.join(' ')}; `
|
|
2806
|
+
+ 'pass advertisingRenderer: readCapacitorAdvertisingPushRenderer to the Capacitor provider; '
|
|
2807
|
+
+ 'and set shellRendersAdvertising: true in notifications/deployment.json before mapping an '
|
|
2808
|
+
+ 'advertising topic to android (see docs/NOTIFICATIONS.md).';
|
|
2809
|
+
}
|
|
2810
|
+
function materializeAndroidAdvertisingRenderer(gameRoot) {
|
|
2811
|
+
const manifestFile = safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`);
|
|
2812
|
+
if (!existsSync(manifestFile) || !lstatSync(manifestFile).isFile()) {
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
const source = readFileSync(manifestFile, 'utf8');
|
|
2816
|
+
const next = applyAndroidManifestAdvertisingRenderer(source);
|
|
2817
|
+
if (next !== source) {
|
|
2818
|
+
writeFileSync(manifestFile, next);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2603
2821
|
function materializeAndroidInexactAlarms(gameRoot) {
|
|
2604
2822
|
const manifestFile = safeDestination(gameRoot, `${shellPath}/android/app/src/main/AndroidManifest.xml`);
|
|
2605
2823
|
if (!existsSync(manifestFile) || !lstatSync(manifestFile).isFile()) {
|
|
@@ -2668,10 +2886,10 @@ function planNotificationsProvider(input) {
|
|
|
2668
2886
|
+ `@mpgd/adapter-capacitor ${String(adapter.version)} does not export; upgrade the game's kit `
|
|
2669
2887
|
+ 'packages (mpgd kit upgrade) first.');
|
|
2670
2888
|
}
|
|
2671
|
-
// The starter's provider module also imports the settings opener
|
|
2672
|
-
// an adapter older than this CLI's
|
|
2673
|
-
// The subpath's declared types file
|
|
2674
|
-
// the
|
|
2889
|
+
// The starter's provider module also imports the settings opener and the
|
|
2890
|
+
// advertising renderer read, which an adapter older than this CLI's
|
|
2891
|
+
// notifications starter does not have. The subpath's declared types file
|
|
2892
|
+
// (dist/notifications.d.ts today), and the modules it re-exports them from.
|
|
2675
2893
|
const adapterRoot = path.join(gameRoot, 'node_modules/@mpgd/adapter-capacitor');
|
|
2676
2894
|
const declaredTypes = adapterExports['./notifications'];
|
|
2677
2895
|
const typesPath = typeof declaredTypes === 'object' && declaredTypes !== null
|
|
@@ -2679,14 +2897,22 @@ function planNotificationsProvider(input) {
|
|
|
2679
2897
|
? declaredTypes.types
|
|
2680
2898
|
: './dist/notifications.d.ts';
|
|
2681
2899
|
const adapterTypes = readExisting(path.join(adapterRoot, typesPath));
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2900
|
+
const helperTypes = ['notifications-settings.d.ts', 'notifications-renderer.d.ts']
|
|
2901
|
+
.map((file) => readExisting(path.join(adapterRoot, path.dirname(typesPath), file)) ?? '')
|
|
2902
|
+
.join('\n');
|
|
2903
|
+
// Refused only when the declarations show a helper is absent: an
|
|
2684
2904
|
// `export *` the check cannot follow passes, and the build then decides.
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2905
|
+
// The advertising renderer read came after the opener (AR1).
|
|
2906
|
+
for (const helper of [
|
|
2907
|
+
'openCapacitorNotificationSettings',
|
|
2908
|
+
'readCapacitorAdvertisingPushRenderer',
|
|
2909
|
+
]) {
|
|
2910
|
+
if (adapterTypes !== undefined && !/\bexport\s*\*/u.test(adapterTypes)
|
|
2911
|
+
&& !`${adapterTypes}\n${helperTypes}`.includes(helper)) {
|
|
2912
|
+
throw new Error(`The game's @mpgd/adapter-capacitor ${String(adapter.version)} has no `
|
|
2913
|
+
+ `${helper}, which the notifications starter uses; upgrade the `
|
|
2914
|
+
+ "game's kit packages (mpgd kit upgrade) first.");
|
|
2915
|
+
}
|
|
2690
2916
|
}
|
|
2691
2917
|
// The adapter's other peers (Capacitor core) must accept the game's
|
|
2692
2918
|
// installed copies too; the plugins get their own message below.
|
|
@@ -2831,6 +3057,12 @@ function planNotificationsProvider(input) {
|
|
|
2831
3057
|
+ '@capacitor/local-notifications adds; reminders are inexact, so add '
|
|
2832
3058
|
+ `${androidInexactAlarmPermission} to the manifest.`);
|
|
2833
3059
|
}
|
|
3060
|
+
const rendererWarning = androidManifest === undefined
|
|
3061
|
+
? undefined
|
|
3062
|
+
: androidAdvertisingRendererWarning(androidManifest);
|
|
3063
|
+
if (rendererWarning !== undefined) {
|
|
3064
|
+
warnings.push(rendererWarning);
|
|
3065
|
+
}
|
|
2834
3066
|
if (readExisting(safeDestination(gameRoot, `${shellPath}/android/app/google-services.json`)) === undefined) {
|
|
2835
3067
|
warnings.push(`Android push needs ${shellPath}/android/app/google-services.json from the Firebase console `
|
|
2836
3068
|
+ '(see the mpgd-kit Firebase guide); without it the push half fails closed.');
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import completion from '@gunshi/plugin-completion';
|
|
@@ -12,7 +12,7 @@ import { buildAssetPacks } from './asset-pack-build.js';
|
|
|
12
12
|
import { verifyAssetPackDelivery } from './asset-pack-verify.js';
|
|
13
13
|
import { buildAssetPackTarget } from './asset-pack-target.js';
|
|
14
14
|
import { publishAssetPacks } from './asset-pack-publish.js';
|
|
15
|
-
import { assertLegalProjectPath, publishLegalSiteFiles } from './legal-site-output.js';
|
|
15
|
+
import { assertLegalProjectPath, publishLegalSiteFiles, resolveAppAdsTxt, } from './legal-site-output.js';
|
|
16
16
|
import { assertAssetPackTargetPolicy } from '@mpgd/target-config/asset-packs';
|
|
17
17
|
import { materializeCapacitorShellStarter, planCapacitorShellStarter, } from './capacitor-shell-starter.js';
|
|
18
18
|
import { doctorNativeDeployment, initializeDeployConfig, parseDeployTargets, planNativeDeployment, readNativeDeploymentPlan, writeNativeDeploymentPlan, } from './deploy-planning.js';
|
|
@@ -973,7 +973,19 @@ const gameCommand = defineI18n({
|
|
|
973
973
|
const defaultLegalDir = 'legal';
|
|
974
974
|
const defaultLegalOutDir = 'artifacts/legal-site';
|
|
975
975
|
const legalPageSlugs = ['privacy', 'support', 'terms'];
|
|
976
|
+
/**
|
|
977
|
+
* Built when its source exists: the account deletion request page whose URL
|
|
978
|
+
* Google Play's Data safety form asks for. A missing one is warned about.
|
|
979
|
+
*/
|
|
980
|
+
const optionalLegalPageSlugs = ['delete-account'];
|
|
976
981
|
const legalPageFileList = legalPageSlugs.map((slug) => `${slug}.html`).join(', ');
|
|
982
|
+
const optionalLegalPageFileList = optionalLegalPageSlugs.map((slug) => `${slug}.html`).join(', ');
|
|
983
|
+
/** The account deletion page's slug, whose URL the Play Data safety form asks for. */
|
|
984
|
+
const deleteAccountLegalSlug = optionalLegalPageSlugs[0];
|
|
985
|
+
/** Where a legal page is published: `<out-dir>/<slug>/index.html`, served at `/<slug>/`. */
|
|
986
|
+
function legalPageOutputFile(outDir, slug) {
|
|
987
|
+
return path.join(outDir, slug, 'index.html');
|
|
988
|
+
}
|
|
977
989
|
function acceptGame(input) {
|
|
978
990
|
if (input.skipTargetBuild && !input.skipTargetSmoke) {
|
|
979
991
|
throw new Error('Use --skip-target-smoke when --skip-target-build is set.');
|
|
@@ -1540,6 +1552,11 @@ const legalCommand = defineI18n({
|
|
|
1540
1552
|
check: false,
|
|
1541
1553
|
});
|
|
1542
1554
|
console.info(`Built legal site: ${result.outDir}`);
|
|
1555
|
+
if (result.deleteAccountPath !== undefined) {
|
|
1556
|
+
console.info(`Account deletion page: ${result.deleteAccountPath} on the legal site. Enter its full URL `
|
|
1557
|
+
+ 'in Play Console > App content > Data safety > Delete account URL, and link it from '
|
|
1558
|
+
+ 'the privacy policy.');
|
|
1559
|
+
}
|
|
1543
1560
|
},
|
|
1544
1561
|
}),
|
|
1545
1562
|
check: defineI18n({
|
|
@@ -2760,7 +2777,7 @@ function legalArgs() {
|
|
|
2760
2777
|
type: 'string',
|
|
2761
2778
|
required: false,
|
|
2762
2779
|
default: defaultLegalDir,
|
|
2763
|
-
description: `Directory containing ${legalPageFileList}.`,
|
|
2780
|
+
description: `Directory containing ${legalPageFileList} (and optionally ${optionalLegalPageFileList}).`,
|
|
2764
2781
|
},
|
|
2765
2782
|
'out-dir': {
|
|
2766
2783
|
type: 'string',
|
|
@@ -2773,8 +2790,8 @@ function legalArgs() {
|
|
|
2773
2790
|
function legalArgResources() {
|
|
2774
2791
|
return {
|
|
2775
2792
|
'legal-dir': {
|
|
2776
|
-
en: `Directory containing ${legalPageFileList}.`,
|
|
2777
|
-
ko: `${legalPageFileList} 이 있는 디렉터리.`,
|
|
2793
|
+
en: `Directory containing ${legalPageFileList} (and optionally ${optionalLegalPageFileList}).`,
|
|
2794
|
+
ko: `${legalPageFileList} (선택: ${optionalLegalPageFileList}) 이 있는 디렉터리.`,
|
|
2778
2795
|
},
|
|
2779
2796
|
'out-dir': {
|
|
2780
2797
|
en: 'Output directory for the static legal site.',
|
|
@@ -2794,8 +2811,58 @@ function buildLegalSite(input) {
|
|
|
2794
2811
|
assertLegalProjectPath('legal-dir', legalDir);
|
|
2795
2812
|
assertLegalProjectPath('out-dir', outDir);
|
|
2796
2813
|
const outputs = createLegalSiteOutputs(legalDir, outDir);
|
|
2814
|
+
const deleteAccountPath = `/${deleteAccountLegalSlug}/`;
|
|
2815
|
+
const deleteAccountBuilt = outputs.some((output) => output.file === legalPageOutputFile(outDir, deleteAccountLegalSlug));
|
|
2816
|
+
const deleteAccountResult = deleteAccountBuilt ? { deleteAccountPath } : {};
|
|
2817
|
+
if (!deleteAccountBuilt) {
|
|
2818
|
+
const linking = outputs
|
|
2819
|
+
// Any quoting of the link (or a full URL ending in the path).
|
|
2820
|
+
.filter((output) => output.content.includes(deleteAccountPath))
|
|
2821
|
+
.map((output) => path.relative(process.cwd(), output.file));
|
|
2822
|
+
const source = path.join(path.relative(process.cwd(), legalDir) || '.', `${deleteAccountLegalSlug}.html`);
|
|
2823
|
+
if (linking.length > 0) {
|
|
2824
|
+
// The starter's privacy, support, and terms pages link to it: a site
|
|
2825
|
+
// without it would publish dead deletion links, so it is required then.
|
|
2826
|
+
throw new Error(`No ${source}, but these pages link to ${deleteAccountPath}: ${linking.join(', ')}. `
|
|
2827
|
+
+ 'Google Play needs that page (Play Console > App content > Data safety): copy '
|
|
2828
|
+
+ 'legal/delete-account.html from a game created with the current kit, or remove the links.');
|
|
2829
|
+
}
|
|
2830
|
+
console.warn(`No ${source}: `
|
|
2831
|
+
+ 'Google Play needs a web page where players request account deletion without reinstalling '
|
|
2832
|
+
+ 'the game (Play Console > App content > Data safety). Copy legal/delete-account.html from a '
|
|
2833
|
+
+ 'game created with the current kit.');
|
|
2834
|
+
}
|
|
2835
|
+
// app-ads.txt and the optional pages are stale once their source is gone.
|
|
2836
|
+
const optionalOutputs = [
|
|
2837
|
+
{ file: path.join(outDir, 'app-ads.txt'), source: 'app-ads.txt' },
|
|
2838
|
+
...optionalLegalPageSlugs.map((slug) => ({
|
|
2839
|
+
file: legalPageOutputFile(outDir, slug),
|
|
2840
|
+
source: `${slug}.html`,
|
|
2841
|
+
})),
|
|
2842
|
+
];
|
|
2843
|
+
const orphans = optionalOutputs.filter(({ file }) => {
|
|
2844
|
+
if (outputs.some((output) => output.file === file)) {
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2847
|
+
const stat = lstatIfExists(file);
|
|
2848
|
+
if (stat === undefined) {
|
|
2849
|
+
return false;
|
|
2850
|
+
}
|
|
2851
|
+
assertLegalProjectPath('out-dir', file);
|
|
2852
|
+
if (!stat.isFile()) {
|
|
2853
|
+
throw new Error(`Legal site output is not a regular file: ${file}`);
|
|
2854
|
+
}
|
|
2855
|
+
return true;
|
|
2856
|
+
});
|
|
2857
|
+
warnLegalPlaceholders(outputs);
|
|
2797
2858
|
if (input.check) {
|
|
2798
2859
|
assertLegalProjectPath('out-dir', outDir);
|
|
2860
|
+
if (orphans.length > 0) {
|
|
2861
|
+
throw new Error([
|
|
2862
|
+
'Legal site output is stale. Run "mpgd legal build".',
|
|
2863
|
+
...orphans.map(({ file, source }) => `- ${path.relative(process.cwd(), file)} (no ${source} source)`),
|
|
2864
|
+
].join('\n'));
|
|
2865
|
+
}
|
|
2799
2866
|
const stale = outputs.filter((output) => {
|
|
2800
2867
|
assertLegalProjectPath('out-dir', output.file);
|
|
2801
2868
|
if (!existsSync(output.file)) {
|
|
@@ -2811,18 +2878,47 @@ function buildLegalSite(input) {
|
|
|
2811
2878
|
}
|
|
2812
2879
|
return {
|
|
2813
2880
|
outDir,
|
|
2881
|
+
...deleteAccountResult,
|
|
2814
2882
|
};
|
|
2815
2883
|
}
|
|
2816
2884
|
publishLegalSiteFiles(outDir, outputs);
|
|
2885
|
+
for (const { file } of orphans) {
|
|
2886
|
+
// Checked again right before removing: the directory may have changed.
|
|
2887
|
+
assertLegalProjectPath('out-dir', file);
|
|
2888
|
+
rmSync(file, { force: true });
|
|
2889
|
+
if (path.dirname(file) !== outDir) {
|
|
2890
|
+
// The page's own directory, once empty.
|
|
2891
|
+
try {
|
|
2892
|
+
rmdirSync(path.dirname(file));
|
|
2893
|
+
}
|
|
2894
|
+
catch (error) {
|
|
2895
|
+
// Not empty, or already gone: leave it. Anything else is reported.
|
|
2896
|
+
const code = error.code;
|
|
2897
|
+
if (code !== 'ENOTEMPTY' && code !== 'EEXIST' && code !== 'ENOENT' && code !== 'ENOTDIR') {
|
|
2898
|
+
console.warn(`Could not remove the empty legal page directory ${path.relative(process.cwd(), path.dirname(file))}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2817
2903
|
return {
|
|
2818
2904
|
outDir,
|
|
2905
|
+
...deleteAccountResult,
|
|
2819
2906
|
};
|
|
2820
2907
|
}
|
|
2821
2908
|
function createLegalSiteOutputs(legalDir, outDir) {
|
|
2822
2909
|
assertLegalProjectPath('legal-dir', legalDir);
|
|
2823
2910
|
assertLegalProjectPath('out-dir', outDir);
|
|
2824
2911
|
const sourceRoot = toTemplatePath(path.relative(process.cwd(), legalDir) || '.');
|
|
2825
|
-
const
|
|
2912
|
+
const optionalSlugs = optionalLegalPageSlugs.filter((slug) => {
|
|
2913
|
+
const sourceFile = path.join(legalDir, `${slug}.html`);
|
|
2914
|
+
if (lstatIfExists(sourceFile) === undefined) {
|
|
2915
|
+
return false;
|
|
2916
|
+
}
|
|
2917
|
+
assertLegalProjectPath('legal-dir', sourceFile);
|
|
2918
|
+
return true;
|
|
2919
|
+
});
|
|
2920
|
+
const slugs = [...legalPageSlugs, ...optionalSlugs];
|
|
2921
|
+
const outputs = slugs.map((slug) => {
|
|
2826
2922
|
const sourceFile = path.join(legalDir, `${slug}.html`);
|
|
2827
2923
|
// existsSync and readFileSync follow a page symlink. Check the file
|
|
2828
2924
|
// itself, not only the legal directory, immediately before reading it.
|
|
@@ -2832,12 +2928,16 @@ function createLegalSiteOutputs(legalDir, outDir) {
|
|
|
2832
2928
|
}
|
|
2833
2929
|
assertLegalProjectPath('legal-dir', sourceFile);
|
|
2834
2930
|
return {
|
|
2835
|
-
file:
|
|
2931
|
+
file: legalPageOutputFile(outDir, slug),
|
|
2836
2932
|
content: normalizeLegalHtml(readFileSync(sourceFile, 'utf8'), sourceFile),
|
|
2837
2933
|
};
|
|
2838
2934
|
});
|
|
2935
|
+
const appAdsTxt = readLegalAppAdsTxt(legalDir);
|
|
2839
2936
|
return [
|
|
2840
2937
|
...outputs,
|
|
2938
|
+
...(appAdsTxt === undefined
|
|
2939
|
+
? []
|
|
2940
|
+
: [{ file: path.join(outDir, 'app-ads.txt'), content: appAdsTxt }]),
|
|
2841
2941
|
{
|
|
2842
2942
|
file: path.join(outDir, '_headers'),
|
|
2843
2943
|
content: [
|
|
@@ -2852,9 +2952,7 @@ function createLegalSiteOutputs(legalDir, outDir) {
|
|
|
2852
2952
|
file: path.join(outDir, '_redirects'),
|
|
2853
2953
|
content: [
|
|
2854
2954
|
'/ /support/ 302',
|
|
2855
|
-
|
|
2856
|
-
'/support /support/ 301',
|
|
2857
|
-
'/terms /terms/ 301',
|
|
2955
|
+
...slugs.map((slug) => `/${slug} /${slug}/ 301`),
|
|
2858
2956
|
'',
|
|
2859
2957
|
].join('\n'),
|
|
2860
2958
|
},
|
|
@@ -2862,7 +2960,7 @@ function createLegalSiteOutputs(legalDir, outDir) {
|
|
|
2862
2960
|
file: path.join(outDir, 'legal-site.json'),
|
|
2863
2961
|
content: `${JSON.stringify({
|
|
2864
2962
|
version: 1,
|
|
2865
|
-
pages:
|
|
2963
|
+
pages: slugs.map((slug) => ({
|
|
2866
2964
|
slug,
|
|
2867
2965
|
path: `/${slug}/`,
|
|
2868
2966
|
source: `${sourceRoot}/${slug}.html`,
|
|
@@ -2871,6 +2969,58 @@ function createLegalSiteOutputs(legalDir, outDir) {
|
|
|
2871
2969
|
},
|
|
2872
2970
|
];
|
|
2873
2971
|
}
|
|
2972
|
+
/**
|
|
2973
|
+
* `app-ads.txt` for the legal site root: `<legal-dir>/app-ads.txt`, else the
|
|
2974
|
+
* AdMob line for `ads.admob.publisherId` in the project's `mpgd.game.json`
|
|
2975
|
+
* (the project root is the working directory, as for the legal sources).
|
|
2976
|
+
*/
|
|
2977
|
+
function readLegalAppAdsTxt(legalDir) {
|
|
2978
|
+
const ownAppAds = path.join(legalDir, 'app-ads.txt');
|
|
2979
|
+
const ownExists = lstatIfExists(ownAppAds) !== undefined;
|
|
2980
|
+
if (ownExists) {
|
|
2981
|
+
assertLegalProjectPath('legal-dir', ownAppAds);
|
|
2982
|
+
}
|
|
2983
|
+
const gameConfigFile = path.join(process.cwd(), 'mpgd.game.json');
|
|
2984
|
+
let gameConfig;
|
|
2985
|
+
if (lstatIfExists(gameConfigFile) !== undefined) {
|
|
2986
|
+
assertLegalProjectPath('mpgd.game.json', gameConfigFile);
|
|
2987
|
+
try {
|
|
2988
|
+
gameConfig = JSON.parse(readFileSync(gameConfigFile, 'utf8'));
|
|
2989
|
+
}
|
|
2990
|
+
catch (error) {
|
|
2991
|
+
throw new Error(`mpgd.game.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return resolveAppAdsTxt({
|
|
2995
|
+
...(ownExists ? { ownFile: readFileSync(ownAppAds, 'utf8') } : {}),
|
|
2996
|
+
...(gameConfig === undefined ? {} : { gameConfig }),
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
function lstatIfExists(file) {
|
|
3000
|
+
try {
|
|
3001
|
+
return lstatSync(file);
|
|
3002
|
+
}
|
|
3003
|
+
catch (error) {
|
|
3004
|
+
const code = error.code;
|
|
3005
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
3006
|
+
return undefined;
|
|
3007
|
+
}
|
|
3008
|
+
throw error;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* The starter's legal pages mark what only the game owner can write with
|
|
3013
|
+
* `[REPLACE: ...]`. Building with them is allowed (a new game builds its
|
|
3014
|
+
* Pages host at once), but the store listing must not link to them.
|
|
3015
|
+
*/
|
|
3016
|
+
function warnLegalPlaceholders(outputs) {
|
|
3017
|
+
const pending = outputs
|
|
3018
|
+
.filter((output) => output.content.includes('[REPLACE:'))
|
|
3019
|
+
.map((output) => path.relative(process.cwd(), output.file));
|
|
3020
|
+
if (pending.length > 0) {
|
|
3021
|
+
console.warn(`Legal pages still contain [REPLACE: ...] markers; fill them in before linking the site from a store listing: ${pending.join(', ')}`);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
2874
3024
|
function normalizeLegalHtml(content, sourceFile) {
|
|
2875
3025
|
if (!/^<!doctype html>/iu.test(content) || !/<html[\s>]/iu.test(content)) {
|
|
2876
3026
|
throw new Error(`${sourceFile} must be a complete HTML document.`);
|
|
@@ -2916,10 +3066,19 @@ function applyLiteTemplateOverlay(baseFiles, overlayFiles) {
|
|
|
2916
3066
|
if (file.relativePath === 'mpgd.targets.json') {
|
|
2917
3067
|
return [{ ...file, content: liteTemplateTargets(file.content) }];
|
|
2918
3068
|
}
|
|
3069
|
+
if (file.relativePath === 'mpgd.game.json') {
|
|
3070
|
+
return [{ ...file, content: liteTemplateGameConfig(file.content) }];
|
|
3071
|
+
}
|
|
2919
3072
|
return [file];
|
|
2920
3073
|
});
|
|
2921
3074
|
return [...merged, ...overlay.values()];
|
|
2922
3075
|
}
|
|
3076
|
+
/** The lite tap game is orientation-agnostic; the Phaser template's landscape lock does not apply. */
|
|
3077
|
+
function liteTemplateGameConfig(content) {
|
|
3078
|
+
const config = JSON.parse(content);
|
|
3079
|
+
config.native = { ...config.native, orientation: 'any' };
|
|
3080
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
3081
|
+
}
|
|
2923
3082
|
function liteTemplateTargets(content) {
|
|
2924
3083
|
const value = JSON.parse(content);
|
|
2925
3084
|
const reddit = value.targets.reddit;
|
|
@@ -10,3 +10,21 @@ export interface LegalSiteFile {
|
|
|
10
10
|
export declare function assertLegalProjectPath(label: string, resolved: string): void;
|
|
11
11
|
/** Stage the site beside the destination, then publish with rename. */
|
|
12
12
|
export declare function publishLegalSiteFiles(outDir: string, outputs: readonly LegalSiteFile[]): void;
|
|
13
|
+
/**
|
|
14
|
+
* Google's AdMob line for `app-ads.txt` (`f08c47fec0942fa0` is Google's
|
|
15
|
+
* certification authority ID). AdMob shows the exact line for an account
|
|
16
|
+
* under Apps > View all apps > app-ads.txt; use a game-owned
|
|
17
|
+
* `legal/app-ads.txt` instead when it differs or mediation partners add lines.
|
|
18
|
+
*/
|
|
19
|
+
export declare function admobAppAdsTxtLine(publisherId: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The `app-ads.txt` the legal site serves at its root, or undefined: a
|
|
22
|
+
* game-owned `<legal-dir>/app-ads.txt` verbatim, else the AdMob line for
|
|
23
|
+
* `ads.admob.publisherId` in the game's `mpgd.game.json`. Store listings
|
|
24
|
+
* name this site as the developer website (Play) or marketing URL (App
|
|
25
|
+
* Store), which is where ad buyers look for the file.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveAppAdsTxt(input: {
|
|
28
|
+
readonly ownFile?: string;
|
|
29
|
+
readonly gameConfig?: unknown;
|
|
30
|
+
}): string | undefined;
|