codeplay-common 4.9.7 → 4.9.9

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.
@@ -0,0 +1,4434 @@
1
+ // Version 6.5: migrate AdMob files to ZIP-distributed folder packages.
2
+ // Version 6.5: recreate failed WebViews, defer background recovery, and suppress foreground recovery toasts.
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const plist = require('plist');
6
+
7
+
8
+
9
+ const { execSync } = require("child_process");
10
+
11
+
12
+
13
+
14
+ const { readFileSync } = require("fs");
15
+ const readline = require('readline');
16
+
17
+ // Await import confirmation before continuing the remaining build steps.
18
+ (async () => {
19
+
20
+ // Standalone builds default to Android on every host OS.
21
+ // npm run build:ios explicitly selects iOS through CODEPLAY_BUILD_PLATFORM.
22
+ const buildPlatform = String(process.env.CODEPLAY_BUILD_PLATFORM || '').trim().toLowerCase() || 'android';
23
+
24
+ if (!['android', 'ios'].includes(buildPlatform)) {
25
+ console.error(`❌ Unsupported build target: ${buildPlatform}. Expected android or ios.`);
26
+ process.exit(1);
27
+ }
28
+
29
+ const ENABLE_AUTO_UPDATE = true;
30
+ const USE_LIVE_SERVER_VERSION = true;
31
+
32
+ // Reuse directory traversal; refresh after generators or plugin downloads alter src.
33
+ let sourceFileInventory = null;
34
+ function getSourceFiles() {
35
+ if (!sourceFileInventory) {
36
+ const sourceRoot = path.join(process.cwd(), 'src');
37
+ sourceFileInventory = fs.existsSync(sourceRoot) ? walkSync(sourceRoot) : [];
38
+ }
39
+ return sourceFileInventory;
40
+ }
41
+
42
+ const configPath = path.join(process.cwd(), 'capacitor.config.json');
43
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
44
+ const updateLogFile = path.join(process.cwd(), "", "plugin-update-log.txt");
45
+ const ONESIGNAL_PLUGIN_NAME = "onesignal-cordova-plugin";
46
+ const { syncAdmobNextgen, syncImports: syncAdmobImports, findLatestPackage: findAdmobPackage } = require('./sync-admob-nextgen.js');
47
+ const ADMOB_NEXTGEN_CONFIG = "admob-ad-configuration-nextgen.json";
48
+ const ADMOB_OLD_CONFIG = "admob-ad-configuration.json";
49
+ const AGENT_INSTRUCTIONS_VERSION_KEY = "agents";
50
+ const AGENT_INSTRUCTIONS_ZIP_PREFIX = "agents";
51
+ const AGENT_INSTRUCTIONS_FILE = "AGENTS.md";
52
+ const AGENT_INSTRUCTIONS_DIR = "AGENTS";
53
+ const AGENT_INSTRUCTIONS_VERSION_FILE = ".codeplay-version";
54
+
55
+ // Remove the retired plugin before any validation, patching or package updates.
56
+ removeOneSignalPlugin();
57
+
58
+ const parseVersionParts = (version) => String(version || "0")
59
+ .trim()
60
+ .split(".")
61
+ .map(part => Number(part) || 0);
62
+
63
+ const extractVersionFromFileName = (fileName) => {
64
+ const match = String(fileName).match(/(\d+(?:\.\d+)*)/);
65
+ return match?.[1] || "0";
66
+ };
67
+
68
+ const compareVersionStrings = (versionA, versionB) => {
69
+ const aParts = parseVersionParts(versionA);
70
+ const bParts = parseVersionParts(versionB);
71
+ const maxParts = Math.max(aParts.length, bParts.length);
72
+
73
+ for (let i = 0; i < maxParts; i++) {
74
+ const diff = (aParts[i] || 0) - (bParts[i] || 0);
75
+ if (diff !== 0) return diff;
76
+ }
77
+ return 0;
78
+ };
79
+
80
+ const getLatestVersionedFileName = (dir, pattern) => {
81
+ if (!fs.existsSync(dir)) return null;
82
+
83
+ const matches = fs.readdirSync(dir).filter((name) => pattern.test(name));
84
+ if (!matches.length) return null;
85
+
86
+ return matches.sort((a, b) => {
87
+ const versionA = extractVersionFromFileName(a);
88
+ const versionB = extractVersionFromFileName(b);
89
+ return compareVersionStrings(versionB, versionA);
90
+ })[0];
91
+ };
92
+
93
+
94
+ // Expected plugin list with minimum versions
95
+ /* const requiredPlugins = [
96
+
97
+ { pattern: /backbutton-(\d+\.\d+)\.js$/, minVersion: '2.0', required: true, baseDir: 'js',mandatoryUpdate: true},
98
+
99
+
100
+ { pattern: /common-(\d+\.\d+)(?:-beta-(\d+))?\.js$/, minVersion: '6.0', required: true, baseDir: 'js',mandatoryUpdate: true },
101
+
102
+ { pattern: /localization_settings-(\d+\.\d+)\.js$/, minVersion: '1.1', required: true, baseDir: 'js',mandatoryUpdate: false },
103
+ { pattern: /localization-(\d+\.\d+)\.js$/, minVersion: '1.5', required: true, baseDir: 'js',mandatoryUpdate: true },
104
+ { pattern: /localNotification-(\d+\.\d+)\.js$/, minVersion: '2.2', required: true, baseDir: 'js',mandatoryUpdate: false },
105
+ { pattern: /localNotification_AppSettings-(\d+\.\d+)\.js$/, minVersion: '1.0', required: true, baseDir: 'js',mandatoryUpdate: false },
106
+ { pattern: /saveToGalleryAndSaveAnyFile-(\d+\.\d+)(-ios)?\.js$/, minVersion: '3.1', required: true, baseDir: 'js',mandatoryUpdate: true },
107
+ { pattern: /Ads[\/\\]admob-emi-(\d+\.\d+)\.js$/, minVersion: '3.7', required: true, baseDir: 'js',mandatoryUpdate: true },
108
+
109
+ // New added plugins
110
+ { pattern: /video-player-(\d+\.\d+)\.js$/, minVersion: '1.5', required: true, baseDir: 'js',mandatoryUpdate: false },
111
+ { pattern: /image-cropper-(\d+\.\d+)\.js$/, minVersion: '1.1', required: true, baseDir: 'js',mandatoryUpdate: false },
112
+ { pattern: /common-(\d+\.\d+)\.less$/, minVersion: '1.6', required: true, baseDir: 'assets/css',mandatoryUpdate: false },
113
+
114
+
115
+ // New folders
116
+ { pattern: /IAP-(\d+\.\d+)$/, minVersion: '2.8', isFolder: true , required: true, baseDir: 'js/Ads',mandatoryUpdate: true },
117
+ { pattern: /editor-(\d+\.\d+)$/, minVersion: '1.9', isFolder: true, required: true, baseDir: 'js',mandatoryUpdate: true },
118
+ { pattern: /ffmpeg-(\d+\.\d+)$/, minVersion: '1.6', isFolder: true, required: true, baseDir: 'js',mandatoryUpdate: true },
119
+ { pattern: /theme-(\d+\.\d+)$/, minVersion: '3.3', isFolder: true , required: true, baseDir: 'theme',mandatoryUpdate: true },
120
+
121
+
122
+ { pattern: /certificatejs-(\d+\.\d+)$/, minVersion: '1.6', isFolder: true , required: true, baseDir: 'certificate',mandatoryUpdate: true }
123
+
124
+ ]; */
125
+
126
+
127
+ const ROOT_DIR = path.join(__dirname, "..", "src");
128
+
129
+ function requireOrInstall(packageName) {
130
+ try {
131
+ return require(packageName);
132
+ } catch (err) {
133
+
134
+ console.log(`📦 "${packageName}" not found. Installing automatically...`);
135
+
136
+ try {
137
+ execSync(`npm install ${packageName}`, { stdio: "inherit" });
138
+ console.log(`✅ "${packageName}" installed successfully.`);
139
+ } catch (installErr) {
140
+ console.error(`❌ Failed to install "${packageName}".`);
141
+ process.exit(1);
142
+ }
143
+
144
+ // Try loading again
145
+ return require(packageName);
146
+ }
147
+ }
148
+
149
+ const AdmZip = requireOrInstall("adm-zip");
150
+
151
+
152
+
153
+ const pkg = require(path.join(process.cwd(), 'node_modules', 'codeplay-common', 'package.json'));
154
+
155
+ const pluginName = pkg.name;
156
+ const pluginVersion = pkg.version;
157
+
158
+ let updateLogs = [];
159
+
160
+ function writeUpdateLine(message) {
161
+ updateLogs.push(message);
162
+ }
163
+
164
+ const MAX_LOG_BLOCKS = 50;
165
+
166
+ function saveUpdateLogs() {
167
+
168
+ if (updateLogs.length === 0) return;
169
+
170
+ const logDir = path.dirname(updateLogFile);
171
+
172
+ if (!fs.existsSync(logDir)) {
173
+ fs.mkdirSync(logDir, { recursive: true });
174
+ }
175
+
176
+ const now = new Date();
177
+
178
+ const formattedTime = now.toLocaleString('en-GB', {
179
+ day: '2-digit',
180
+ month: '2-digit',
181
+ year: 'numeric',
182
+ hour: '2-digit',
183
+ minute: '2-digit',
184
+ hour12: true
185
+ }).replace(',', '').replace(/\//g, '-');
186
+
187
+ let newBlock = `${pluginName}: ${pluginVersion}\n[${formattedTime}]\n`;
188
+
189
+ updateLogs.forEach(line => {
190
+ newBlock += `${line}\n`;
191
+ });
192
+
193
+ newBlock += "\n";
194
+
195
+ let existingLog = "";
196
+
197
+ if (fs.existsSync(updateLogFile)) {
198
+ existingLog = fs.readFileSync(updateLogFile, "utf8");
199
+ }
200
+
201
+ let combinedLog = newBlock + existingLog;
202
+
203
+ // Split blocks by plugin header
204
+ const blocks = combinedLog.split(/\n(?=codeplay-common:)/);
205
+
206
+ // Keep only latest 50
207
+ const trimmed = blocks.slice(0, MAX_LOG_BLOCKS).join("\n");
208
+
209
+ fs.writeFileSync(updateLogFile, trimmed);
210
+
211
+ }
212
+
213
+
214
+
215
+
216
+
217
+ const versionsFile = path.join(__dirname, "versions.json");
218
+
219
+ function loadRequiredPlugins() {
220
+
221
+ if (!fs.existsSync(versionsFile)) {
222
+ console.error("❌ versions.json not found");
223
+ process.exit(1);
224
+ }
225
+
226
+ const json = JSON.parse(fs.readFileSync(versionsFile, "utf8"));
227
+
228
+ // OneSignal is retired; do not download its legacy helpers again.
229
+ return json.plugins.filter(p =>
230
+ !/^admob-(?:emi-)?nextgen$/i.test(String(p.name || '')) && !/^onesignal(?:-|$)/i.test(String(p.name || '')) && !/onesignal/i.test(String(p.pattern || ''))
231
+ ).map(p => ({
232
+ ...p,
233
+ pattern: new RegExp(p.pattern)
234
+ }));
235
+
236
+ }
237
+
238
+ let requiredPlugins = loadRequiredPlugins();
239
+
240
+
241
+
242
+
243
+
244
+
245
+ async function downloadAndExtractZip(url, destFolder) {
246
+
247
+ const zipPath = destFolder + ".zip";
248
+
249
+ await downloadFile(url, zipPath);
250
+
251
+ const zip = new AdmZip(zipPath);
252
+ zip.extractAllTo(destFolder, true);
253
+
254
+ fs.unlinkSync(zipPath);
255
+
256
+ }
257
+
258
+
259
+
260
+
261
+
262
+
263
+
264
+ //Check codeplay-common latest version installed or not Start
265
+ //const { execSync } = require('child_process');
266
+
267
+
268
+ function getInstalledVersion(packageName) {
269
+ try {
270
+ const packageJsonPath = path.join(process.cwd(), 'node_modules', packageName, 'package.json');
271
+ if (fs.existsSync(packageJsonPath)) {
272
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
273
+ return packageJson.version;
274
+ }
275
+ } catch (error) {
276
+ return null;
277
+ }
278
+ return null;
279
+ }
280
+
281
+ function getLatestVersion(packageName) {
282
+ try {
283
+ return execSync(`npm view ${packageName} version`).toString().trim();
284
+ } catch (error) {
285
+ console.error(`Failed to fetch latest version for ${packageName}`);
286
+ return null;
287
+ }
288
+ }
289
+
290
+ function checkPackageVersion() {
291
+ const packageName = 'codeplay-common';
292
+ const installedVersion = getInstalledVersion(packageName);
293
+ const latestVersion = getLatestVersion(packageName);
294
+
295
+ if (!installedVersion) {
296
+ console.error(`${packageName} is not installed. Installing now...`);
297
+ execSync(`npm install ${packageName}`, { stdio: 'inherit' });
298
+ return;
299
+ }
300
+
301
+ if (installedVersion !== latestVersion) {
302
+ console.log(`${packageName} is outdated (installed: ${installedVersion}, latest: ${latestVersion}). Updating now...`);
303
+
304
+ try {
305
+ execSync(`npm uninstall ${packageName}`, { stdio: 'inherit' });
306
+ execSync(`npm install ${packageName}`, { stdio: 'inherit' });
307
+ console.log(`${packageName} successfully updated.`);
308
+ } catch (err) {
309
+ console.error(`Failed to update ${packageName}:`, err.message);
310
+ process.exit(1);
311
+ }
312
+
313
+ return;
314
+ }
315
+
316
+ console.log(`${packageName} is up to date (version ${installedVersion}).`);
317
+ }
318
+
319
+ // Run package version check before executing the main script
320
+ try {
321
+ checkPackageVersion();
322
+ } catch (error) {
323
+ console.error(error.message);
324
+ process.exit(1);
325
+ }
326
+
327
+ //Check codeplay-common latest version installed or not END
328
+
329
+
330
+
331
+ function compareWithBeta(installedVersion, minVersion, isBeta) {
332
+ const baseCompare = compareVersions(installedVersion, minVersion);
333
+
334
+ if (!isBeta) {
335
+ // Stable version → normal compare
336
+ return baseCompare;
337
+ }
338
+
339
+ // Beta version logic
340
+ if (baseCompare > 0) return 1; // 5.3-beta > 5.2
341
+ if (baseCompare < 0) return -1; // 5.1-beta < 5.2
342
+
343
+ // Same version but beta → LOWER than stable
344
+ return -1; // 5.2-beta < 5.2
345
+ }
346
+
347
+
348
+
349
+
350
+ const ensureAnalyticsFlagsInConfig = (config) => {
351
+ const analyticsFlagKey = 'ANALYTICS_ENABLED';
352
+ const legacyAnalyticsFlagKey = ['ANALY', 'STICS_ENABLED'].join('');
353
+ let updated = false;
354
+
355
+ if (!config.android) {
356
+ config.android = {};
357
+ updated = true;
358
+ }
359
+
360
+ if (!config.ios) {
361
+ config.ios = {};
362
+ updated = true;
363
+ }
364
+
365
+ const syncPlatformFlag = (platformName, platformConfig) => {
366
+ const existingValue = platformConfig[analyticsFlagKey];
367
+ const legacyValue = platformConfig[legacyAnalyticsFlagKey];
368
+ let nextValue = false;
369
+
370
+ if (typeof existingValue === 'boolean') {
371
+ nextValue = existingValue;
372
+ } else if (typeof legacyValue === 'boolean') {
373
+ nextValue = legacyValue;
374
+ } else if (existingValue !== undefined) {
375
+ console.warn(`⚠️ ${platformName}.ANALYTICS_ENABLED should be true or false. Automatically setting it to false.`);
376
+ }
377
+
378
+ if (platformConfig[analyticsFlagKey] !== nextValue) {
379
+ platformConfig[analyticsFlagKey] = nextValue;
380
+ updated = true;
381
+ console.warn(`⚠️ ${platformName}.ANALYTICS_ENABLED added/updated as ${nextValue}.`);
382
+ }
383
+
384
+ if (legacyAnalyticsFlagKey in platformConfig) {
385
+ delete platformConfig[legacyAnalyticsFlagKey];
386
+ updated = true;
387
+ console.warn(`⚠️ ${platformName}.ANALYTICS_ENABLED spelling corrected.`);
388
+ }
389
+
390
+ if (nextValue === false) {
391
+ console.warn(`⚠️ Google Analytics is disabled for ${platformName}. Mostly recommended to enable it when IAP is available.`);
392
+ }
393
+ };
394
+
395
+ syncPlatformFlag('android', config.android);
396
+ syncPlatformFlag('ios', config.ios);
397
+
398
+ return updated;
399
+ };
400
+
401
+ const checkAppUniqueId=()=>{
402
+
403
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
404
+ const analyticsFlagsUpdated = ensureAnalyticsFlagsInConfig(config);
405
+
406
+ const appUniqueId = config.android?.APP_UNIQUE_ID;
407
+ const RESIZEABLE_ACTIVITY = config.android?.RESIZEABLE_ACTIVITY;
408
+ const orientation = config.android?.ORIENTATION;
409
+
410
+
411
+ let logErrorMessage="";
412
+
413
+ // 1️⃣ Check if it’s missing
414
+ if (RESIZEABLE_ACTIVITY === undefined) {
415
+ logErrorMessage+='❌ Missing android.RESIZEABLE_ACTIVITY option in capacitor.config.json.\n';
416
+ }
417
+
418
+ // 2️⃣ Check if it’s not boolean (true/false only)
419
+ else if (typeof RESIZEABLE_ACTIVITY !== 'boolean') {
420
+ logErrorMessage+='❌ Invalid android.RESIZEABLE_ACTIVITY value. Please use only true or false (without quotes).\n';
421
+ }
422
+
423
+
424
+
425
+ if (!orientation) {
426
+ logErrorMessage+='❌ Missing android.ORIENTATION option in capacitor.config.json.\n';
427
+ }
428
+
429
+ else if(orientation!="portrait" && orientation!="landscape" && orientation!="auto")
430
+ {
431
+ logErrorMessage+='❌ Spelling mistake in android.ORIENTATION option in capacitor.config.json. Please use only ["portrait" "landscape" "auto"]\n';
432
+ }
433
+
434
+
435
+ if (!appUniqueId) {
436
+ logErrorMessage+='❌ APP_UNIQUE_ID is missing in capacitor.config.json.';
437
+ }
438
+
439
+ else if (!Number.isInteger(appUniqueId)) {
440
+ logErrorMessage+='❌ APP_UNIQUE_ID must be an integer. Example: 1, 2, 3, etc.';
441
+ }
442
+
443
+ if(logErrorMessage!="")
444
+ {
445
+ if (analyticsFlagsUpdated) {
446
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
447
+ console.log('✅ capacitor.config.json analytics flags synced.');
448
+ }
449
+
450
+ console.error(logErrorMessage);
451
+ process.exit(1)
452
+ }
453
+
454
+ if (analyticsFlagsUpdated) {
455
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
456
+ console.log('✅ capacitor.config.json analytics flags synced.');
457
+ }
458
+
459
+
460
+ console.log(`✅ APP_UNIQUE_ID is valid: ${appUniqueId}`);
461
+ warnForNonAutoOrientationConfig(orientation, RESIZEABLE_ACTIVITY);
462
+
463
+ }
464
+
465
+ function readBuildConfirmationInput(question) {
466
+ process.stdout.write(question);
467
+
468
+ const buffer = Buffer.alloc(1024);
469
+ let answer = "";
470
+
471
+ while (!answer.includes("\n")) {
472
+ const bytesRead = fs.readSync(0, buffer, 0, buffer.length, null);
473
+
474
+ if (bytesRead === 0) {
475
+ return null;
476
+ }
477
+
478
+ answer += buffer.toString("utf8", 0, bytesRead);
479
+ }
480
+
481
+ return answer.trim().toLowerCase();
482
+ }
483
+
484
+ function warnForNonAutoOrientationConfig(orientation, RESIZEABLE_ACTIVITY) {
485
+ if (orientation === "auto" && RESIZEABLE_ACTIVITY === true) {
486
+ return;
487
+ }
488
+
489
+ console.warn(`
490
+ ⚠️ WARNING: Android orientation/resizable settings are not set to the recommended values.
491
+
492
+ Current:
493
+ android.ORIENTATION: ${orientation}
494
+ android.RESIZEABLE_ACTIVITY: ${RESIZEABLE_ACTIVITY}
495
+
496
+ Recommended:
497
+ android.ORIENTATION: auto
498
+ android.RESIZEABLE_ACTIVITY: true
499
+
500
+ Press Enter, y, or yes to continue.
501
+ Press n, no, or not to stop the build.
502
+ `);
503
+
504
+ while (true) {
505
+ const answer = readBuildConfirmationInput("Continue build? (Y/n): ");
506
+
507
+ if (answer === "" || answer === "y" || answer === "yes") {
508
+ console.log("✅ Continuing build...");
509
+ return;
510
+ }
511
+
512
+ if (answer === null) {
513
+ console.log("❌ Build cancelled because no user input was received.");
514
+ process.exit(1);
515
+ }
516
+
517
+ if (answer === "n" || answer === "no" || answer === "not") {
518
+ console.log("❌ Build cancelled due to orientation/resizable settings.");
519
+ process.exit(1);
520
+ }
521
+
522
+ console.log("Please enter y, yes, n, no, not, or press Enter.");
523
+ }
524
+ }
525
+
526
+ checkAppUniqueId();
527
+
528
+
529
+
530
+
531
+
532
+
533
+ // ======================================================
534
+ // ENSURE IAP STARTS FROM THE HOME PAGE
535
+ // ======================================================
536
+
537
+ const getHomePagePath = () => {
538
+ const pagesDir = path.join(ROOT_DIR, "pages");
539
+ if (!fs.existsSync(pagesDir)) return null;
540
+
541
+ const exactHomePath = path.join(pagesDir, "home.f7");
542
+ if (fs.existsSync(exactHomePath)) return exactHomePath;
543
+
544
+ const pageFiles = fs.readdirSync(pagesDir)
545
+ .filter(fileName => fileName.endsWith(".f7"));
546
+
547
+ return pageFiles
548
+ .map(fileName => path.join(pagesDir, fileName))
549
+ .find(filePath => /data-name\s*=\s*['"]home['"]/.test(fs.readFileSync(filePath, "utf8"))) || null;
550
+ };
551
+
552
+ const getRelativeModuleImport = (sourceFile, targetFile) => {
553
+ let relativePath = path.relative(path.dirname(sourceFile), targetFile).replace(/\\/g, "/");
554
+ if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
555
+ return relativePath;
556
+ };
557
+
558
+ const addInitIapToExistingImport = (content) => {
559
+ const iapImportRegex = /import\s*\{([^}]*)\}\s*from\s*(['"])([^'"]*\/IAP-\d+(?:\.\d+)*\/IAP-check-And-LoadAd\.js)\2\s*;?/;
560
+ const match = content.match(iapImportRegex);
561
+ if (!match || /\binitIAP\b/.test(match[1])) return content;
562
+
563
+ const importNames = match[1].trim();
564
+ const nextImportNames = importNames ? `initIAP, ${importNames}` : "initIAP";
565
+ return content.replace(iapImportRegex, `import { ${nextImportNames} } from ${match[2]}${match[3]}${match[2]};`);
566
+ };
567
+
568
+ const addInitIapCall = (content) => {
569
+ if (/\binitIAP\s*\(/.test(content)) return content;
570
+
571
+ const pageInitRegex = /(\$on\(\s*(['"])pageInit\2\s*,\s*(?:async\s*)?\(\)\s*=>\s*\{\s*\r?\n)/;
572
+ if (pageInitRegex.test(content)) {
573
+ return content.replace(pageInitRegex, "$1 initIAP();\n");
574
+ }
575
+
576
+ const componentStartRegex = /(export\s+default\s*\([^)]*\)\s*=>\s*\{\s*\r?\n)/;
577
+ if (componentStartRegex.test(content)) {
578
+ return content.replace(componentStartRegex, "$1 initIAP();\n");
579
+ }
580
+
581
+ throw new Error("Unable to find a Framework7 component or pageInit lifecycle for IAP startup.");
582
+ };
583
+
584
+ const ensureIapStartupOnHomePage = () => {
585
+ const iapConfig = getAdmobIapConfig()?.IAP;
586
+ if (!isAnyIapStoreEnabled(iapConfig)) {
587
+ console.log("ℹ️ IAP is disabled for every store. AdMob will use direct startup.");
588
+ return;
589
+ }
590
+
591
+ const latestIapDir = getLatestIapDir();
592
+ if (!latestIapDir) {
593
+ throw new Error("IAP is enabled but no IAP-x.x folder was found.");
594
+ }
595
+
596
+ const homePagePath = getHomePagePath();
597
+ if (!homePagePath) {
598
+ throw new Error("IAP is enabled but a Framework7 Home page could not be found in src/pages.");
599
+ }
600
+
601
+ const iapModulePath = path.join(latestIapDir, "IAP-check-And-LoadAd.js");
602
+ if (!fs.existsSync(iapModulePath)) {
603
+ throw new Error(`IAP startup module is missing: ${iapModulePath}`);
604
+ }
605
+
606
+ let content = fs.readFileSync(homePagePath, "utf8");
607
+ const originalContent = content;
608
+ const staticInitImportRegex = /import\s*\{[^}]*\binitIAP\b[^}]*\}\s*from\s*['"][^'"]*\/IAP-\d+(?:\.\d+)*\/IAP-check-And-LoadAd\.js['"]\s*;?/;
609
+
610
+ content = addInitIapToExistingImport(content);
611
+
612
+ if (!staticInitImportRegex.test(content)) {
613
+ const relativeImport = getRelativeModuleImport(homePagePath, iapModulePath);
614
+ const scriptStartRegex = /<script>\s*\r?\n/;
615
+ if (!scriptStartRegex.test(content)) {
616
+ throw new Error(`Unable to find the script block in ${homePagePath}`);
617
+ }
618
+ content = content.replace(scriptStartRegex, `$&import { initIAP } from '${relativeImport}';\n`);
619
+ }
620
+
621
+ content = addInitIapCall(content);
622
+
623
+ if (content !== originalContent) {
624
+ fs.writeFileSync(homePagePath, content, "utf8");
625
+ console.log(`✅ Added direct IAP startup to ${path.relative(process.cwd(), homePagePath)}.`);
626
+ } else {
627
+ console.log(`ℹ️ Direct IAP startup already configured in ${path.relative(process.cwd(), homePagePath)}.`);
628
+ }
629
+ };
630
+
631
+ // ======================================================
632
+ // ENSURE IAP STARTS FROM THE HOME PAGE END
633
+ // ======================================================
634
+
635
+
636
+
637
+
638
+
639
+
640
+
641
+
642
+
643
+
644
+
645
+ //@Codemirror check and install/uninstall the packages START
646
+ //const fs = require("fs");
647
+ //const path = require("path");
648
+ //const { execSync } = require("child_process");
649
+
650
+ const jsDir = path.join(__dirname, "..", "src", "js");
651
+
652
+ // 🔍 Detect OLD structure
653
+ const oldEditorDirs = fs.readdirSync(jsDir)
654
+ .filter(name => /^editor-\d+\.\d+$/.test(name));
655
+
656
+ // 📁 New structure path (optional, not mandatory)
657
+ const newEditorBaseDir = path.join(jsDir, "editor");
658
+
659
+ // ======================================================
660
+ // ❌ CASE 1: OLD STRUCTURE FOUND → STOP
661
+ // ======================================================
662
+ if (oldEditorDirs.length > 0) {
663
+
664
+ console.error(`
665
+ ❌ OLD EDITOR STRUCTURE DETECTED
666
+
667
+ You are using outdated folder structure:
668
+ src/js/editor-x.x/
669
+
670
+ 🚨 This is no longer supported.
671
+
672
+ 📦 Found folders:
673
+ ${oldEditorDirs.map(d => " - " + d).join("\n")}
674
+
675
+ 👉 Please move them manually:
676
+
677
+ src/js/editor-1.6
678
+ ⬇
679
+ src/js/editor/editor-1.6
680
+
681
+ ⚠️ Also ensure:
682
+ src/js/editor/configuration.json exists
683
+
684
+ ❌ Build stopped.
685
+ `);
686
+
687
+ process.exit(1);
688
+ }
689
+
690
+ // ======================================================
691
+ // ✅ CASE 2: NEW STRUCTURE EXISTS → VALIDATE
692
+ // ======================================================
693
+ if (fs.existsSync(newEditorBaseDir)) {
694
+
695
+ // 🔍 Find editor-x.x inside new folder
696
+ const editorDirs = fs.readdirSync(newEditorBaseDir)
697
+ .filter(name => /^editor-\d+\.\d+$/.test(name));
698
+
699
+ // 👉 If editor folder exists but no versions → skip safely
700
+ if (editorDirs.length === 0) {
701
+ console.log("ℹ️ No editor-x.x folders found inside src/js/editor/");
702
+ return;
703
+ }
704
+
705
+ // ======================================================
706
+ // ✅ Validate configuration.json (NEW RULE)
707
+ // ======================================================
708
+
709
+ const editorConfigPath = path.join(newEditorBaseDir, "configuration.json");
710
+
711
+ // ❌ Missing config
712
+ if (!fs.existsSync(editorConfigPath)) {
713
+ console.error(`
714
+ ❌ MISSING EDITOR CONFIGURATION FILE
715
+
716
+ Required:
717
+ src/js/editor/configuration.json
718
+
719
+ ❌ Build stopped.
720
+ `);
721
+ process.exit(1);
722
+ }
723
+
724
+ // ❌ Check wrong placement
725
+ const invalidConfigs = [];
726
+
727
+ editorDirs.forEach(dir => {
728
+ const wrongPath = path.join(newEditorBaseDir, dir, "configuration.json");
729
+ if (fs.existsSync(wrongPath)) {
730
+ invalidConfigs.push(`src/js/editor/${dir}/configuration.json`);
731
+ }
732
+ });
733
+
734
+ if (invalidConfigs.length > 0) {
735
+ console.error(`
736
+ ❌ INVALID CONFIGURATION LOCATION
737
+
738
+ 🚫 configuration.json must NOT be inside version folders.
739
+
740
+ Found:
741
+ ${invalidConfigs.map(p => " - " + p).join("\n")}
742
+
743
+ ✅ Correct:
744
+ src/js/editor/configuration.json
745
+
746
+ ❌ Build stopped.
747
+ `);
748
+ process.exit(1);
749
+ }
750
+
751
+ console.log("✅ Editor structure validated.");
752
+
753
+ // ======================================================
754
+ // 🚀 Continue execution (run.js)
755
+ // ======================================================
756
+
757
+ const latestEditorDir = editorDirs.sort((a, b) => {
758
+ const vA = parseFloat(a.split('-')[1]);
759
+ const vB = parseFloat(b.split('-')[1]);
760
+ return vB - vA;
761
+ })[0];
762
+
763
+ const runJsPath = path.join(newEditorBaseDir, latestEditorDir, "run.js");
764
+
765
+ if (!fs.existsSync(runJsPath)) {
766
+ console.error(`❌ run.js not found in ${latestEditorDir}`);
767
+ process.exit(1);
768
+ }
769
+
770
+ console.log(`🚀 Executing ${runJsPath}...`);
771
+ execSync(`node "${runJsPath}"`, { stdio: "inherit" });
772
+ }
773
+
774
+ // ======================================================
775
+ // ✅ CASE 3: NOTHING EXISTS → DO NOTHING
776
+ // ======================================================
777
+ else {
778
+ console.log("ℹ️ Editor not used in this project. Skipping...");
779
+ }
780
+
781
+ //@Codemirror check and install/uninstall the packages END
782
+
783
+
784
+
785
+
786
+
787
+
788
+
789
+
790
+
791
+ // Validate saveToGalleryAndSaveAnyFile imports for the build platform START
792
+
793
+ const saveToGalleryAndSaveFileCheck_iOS = async () => {
794
+
795
+ const SOURCE_ROOT = path.resolve(__dirname, '../src');
796
+ const EXCLUDED_DIR = path.join(SOURCE_ROOT, 'js', 'Ads');
797
+ const ANDROID_MANIFEST_PATH = path.resolve(__dirname, '../android/app/src/main/AndroidManifest.xml');
798
+
799
+
800
+ // Skip comments and unrelated strings; match static/dynamic imports, re-exports and require.
801
+ const COMMENT_OR_STRING_REGEX = /\/\/[^\r\n]*|\/\*[\s\S]*?\*\/|(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1/g;
802
+ const IMPORT_REGEX = /(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1|\b(?:import\s+(?:[^;'"`]*?\s+from\s*)?|export\s+[^;'"`]*?\s+from\s*|(?:import|require)\s*\(\s*)(['"])([^'"\r\n]+)\2/g;
803
+ const SAVE_FILE_REGEX = /^(.*[\/\\])?(saveToGalleryAndSaveAnyFile-\d+(?:\.\d+)*)(-ios)?(\.js)?([?#].*)?$/;
804
+ const pendingChanges = [];
805
+
806
+ const ALLOWED_EXTENSIONS = ['.js', '.mjs', '.ts', '.f7'];
807
+ const referencedVersions = new Set();
808
+ let aliasImportFound = false;
809
+ let hasForbiddenEditorImport = false;
810
+ const FORBIDDEN_REGEX = /editor\/editor-\d+(\.\d+)+/;
811
+ const ERROR_MESSAGE = `const ERROR_MESSAGE = ❌ Invalid import detected!
812
+
813
+ You are using a direct version-based path like: editor/editor-x.x/editor.js
814
+
815
+ 🚫 This is NOT allowed.
816
+
817
+ 👉 Please use the proper alias or updated import method.
818
+ Example: import { ... } from '@editor'
819
+
820
+ ⚠️ Do not use version-based paths in imports.
821
+ 👉 Please add this manually in vite.config.js:
822
+
823
+ alias: {
824
+ '@editor': path.resolve(__dirname, './src/js/editor/editor-x.x')
825
+ }`
826
+
827
+ const isIOSBuild = buildPlatform === 'ios';
828
+
829
+ let iosImportFound = false;
830
+ let androidImportFound = false;
831
+
832
+ // Files to skip completely (full or partial match)
833
+ const SKIP_FILES = [
834
+ 'pdf-3.11.174.min.js',
835
+ 'pdf.worker-3.11.174.min.js'
836
+ ,'index.browser.js'
837
+ ];
838
+
839
+
840
+
841
+ const STATUSBAR_FILE = path.resolve(__dirname, '../src/js/common-configuration.js');
842
+ const STATUSBAR_REGEX = /^(?!\s*\/\/).*Capacitor\.Plugins\.StatusBar\.setStyle/m;
843
+
844
+
845
+ function scanSourceFile(dir) {
846
+ /*
847
+ //######################### DO NOT DELETE THIS - START [Appid base validation] #####################################
848
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
849
+ const appUniqueId = config.android?.APP_UNIQUE_ID;
850
+ if (appUniqueId == "206") return;
851
+ //######################### DO NOT DELETE THIS - END [Appid base validation] #####################################
852
+ */
853
+
854
+ const baseName = path.basename(dir);
855
+ const isGalleryScope = ['certificate', 'pages', 'js'].some(folder =>
856
+ dir.startsWith(path.join(SOURCE_ROOT, folder) + path.sep)
857
+ ) || dir === path.join(SOURCE_ROOT, 'app.f7');
858
+ const shouldCheckGallery = isGalleryScope && !dir.startsWith(EXCLUDED_DIR + path.sep)
859
+ && !dir.split(path.sep).includes('node_modules') && !SKIP_FILES.includes(baseName)
860
+ && ALLOWED_EXTENSIONS.some(ext => dir.endsWith(ext));
861
+ const shouldCheckEditor = /\.(js|ts|f7)$/.test(dir);
862
+ const shouldCheckOneSignal = /\.(js|mjs|cjs|ts|tsx|jsx|f7)$/.test(dir);
863
+ if (!shouldCheckGallery && !shouldCheckEditor && !shouldCheckOneSignal) return;
864
+
865
+ const content = fs.readFileSync(dir, 'utf8');
866
+ if (shouldCheckEditor) {
867
+ content.split('\n').forEach((line, index) => {
868
+ if (!FORBIDDEN_REGEX.test(line)) return;
869
+ console.error(`❌ Forbidden import found:\nFile: ${dir}\nLine: ${index + 1}\nCode: ${line.trim()}\n`, ERROR_MESSAGE);
870
+ hasForbiddenEditorImport = true;
871
+ });
872
+ }
873
+ if (dir === STATUSBAR_FILE) {
874
+ const contentWithoutComments = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
875
+ if (contentWithoutComments.includes('Capacitor.Plugins.StatusBar.setStyle')) {
876
+ throw new Error(`❌ StatusBar.setStyle should NOT be used in common-configuration.js. File: ${dir}`);
877
+ }
878
+ }
879
+
880
+ const code = content.replace(COMMENT_OR_STRING_REGEX, (match, quote) =>
881
+ quote ? match : match.replace(/[^\r\n]/g, ' ')
882
+ );
883
+ const replacements = [];
884
+ IMPORT_REGEX.lastIndex = 0;
885
+ let imported;
886
+ while ((imported = IMPORT_REGEX.exec(code)) !== null) {
887
+ if (!imported[3]) continue;
888
+ const importPath = imported[3];
889
+ if (importPath === ONESIGNAL_PLUGIN_NAME || importPath.startsWith(ONESIGNAL_PLUGIN_NAME + '/')) {
890
+ throw new Error(`❌ OneSignal was removed, but ${dir} still imports ${importPath}. Remove that import and its initialization before building.`);
891
+ }
892
+ if (!shouldCheckGallery) continue;
893
+ if (/^@saveGallery[?#]/.test(importPath)) {
894
+ throw new Error(`❌ ${importPath} in ${dir} requires a dedicated Vite alias for its query/hash suffix.`);
895
+ }
896
+ if (importPath === '@saveGallery') {
897
+ aliasImportFound = true;
898
+ continue;
899
+ }
900
+ const fileMatch = importPath.match(SAVE_FILE_REGEX);
901
+ if (!fileMatch) continue;
902
+ if (fileMatch[5]) {
903
+ throw new Error(`❌ Cannot migrate ${importPath} in ${dir} to a plain @saveGallery alias: its query/hash suffix requires a dedicated Vite alias.`);
904
+ }
905
+ referencedVersions.add(fileMatch[2]);
906
+ const nextImportPath = '@saveGallery';
907
+ const offset = imported.index + imported[0].lastIndexOf(imported[2] + importPath + imported[2]) + 1;
908
+ replacements.push({ offset, importPath, nextImportPath });
909
+ }
910
+
911
+ if (replacements.length) {
912
+ let nextContent = content;
913
+ for (const replacement of [...replacements].reverse()) {
914
+ nextContent = nextContent.slice(0, replacement.offset) + replacement.nextImportPath
915
+ + nextContent.slice(replacement.offset + replacement.importPath.length);
916
+ }
917
+ pendingChanges.push({ filePath: dir, content, nextContent, replacements });
918
+ }
919
+ }
920
+
921
+ // One source content pass handles gallery migration, retired OneSignal imports, StatusBar and editor validation.
922
+ for (const sourceFile of getSourceFiles()) scanSourceFile(sourceFile);
923
+ if (hasForbiddenEditorImport) throw new Error('🚫 Build failed due to forbidden editor imports.');
924
+ console.log('✅ No forbidden editor imports found.');
925
+
926
+ if (referencedVersions.size || aliasImportFound) {
927
+ const viteConfigPath = getViteConfigPath();
928
+ if (!viteConfigPath) throw new Error('❌ Cannot configure @saveGallery: vite.config.js or vite.config.mjs is missing.');
929
+ const viteContent = fs.readFileSync(viteConfigPath, 'utf8');
930
+ const viteCode = viteContent.replace(COMMENT_OR_STRING_REGEX, (match, quote) =>
931
+ quote ? match : match.replace(/[^\r\n]/g, ' ')
932
+ );
933
+ const aliasRegex = /(['"])@saveGallery\1\s*:\s*path\.resolve\(\s*__dirname\s*,\s*(['"])([^'"]+)\2\s*\)/g;
934
+ const aliasMatches = [...viteCode.matchAll(aliasRegex)];
935
+ if (aliasMatches.length > 1) throw new Error('❌ Multiple active @saveGallery alias entries found. Keep one entry.');
936
+ const aliasMatch = aliasMatches[0];
937
+ if (!aliasMatch && /['"]@saveGallery['"]\s*:/.test(viteCode)) {
938
+ throw new Error('❌ Unsupported @saveGallery alias format. Use path.resolve(__dirname, a literal file path).');
939
+ }
940
+ if (aliasImportFound && aliasMatch) {
941
+ const existingName = path.basename(aliasMatch[3]).match(SAVE_FILE_REGEX);
942
+ if (!existingName) throw new Error('❌ @saveGallery must point to a versioned saveToGalleryAndSaveAnyFile helper.');
943
+ referencedVersions.add(existingName[2]);
944
+ }
945
+ if (!referencedVersions.size) {
946
+ const jsDir = path.join(SOURCE_ROOT, 'js');
947
+ const pattern = isIOSBuild ? /^saveToGalleryAndSaveAnyFile-\d+(?:\.\d+)*-ios\.js$/ : /^saveToGalleryAndSaveAnyFile-\d+(?:\.\d+)*\.js$/;
948
+ const latestFile = getLatestVersionedFileName(jsDir, pattern);
949
+ if (!latestFile) throw new Error(`❌ No ${buildPlatform} save-gallery helper found in src/js.`);
950
+ referencedVersions.add(latestFile.match(SAVE_FILE_REGEX)[2]);
951
+ }
952
+ if (referencedVersions.size !== 1) {
953
+ throw new Error(`❌ @saveGallery cannot represent multiple helper versions: ${[...referencedVersions].join(', ')}. Align them before building.`);
954
+ }
955
+ const baseName = [...referencedVersions][0];
956
+ const targetRelativePath = `./src/js/${baseName}${isIOSBuild ? '-ios' : ''}.js`;
957
+ const targetPath = path.resolve(path.dirname(viteConfigPath), targetRelativePath);
958
+ if (!fs.existsSync(targetPath)) throw new Error(`❌ Cannot configure @saveGallery: target file does not exist: ${targetPath}`);
959
+ const aliasEntry = `'@saveGallery': path.resolve(__dirname, '${targetRelativePath}')`;
960
+ let nextViteContent = viteContent;
961
+ if (aliasMatch) {
962
+ if (path.resolve(path.dirname(viteConfigPath), aliasMatch[3]) !== targetPath) {
963
+ nextViteContent = viteContent.slice(0, aliasMatch.index) + aliasEntry + viteContent.slice(aliasMatch.index + aliasMatch[0].length);
964
+ }
965
+ } else {
966
+ const aliasHeader = /\balias\s*:\s*\{/g.exec(viteCode);
967
+ const resolveHeader = /\bresolve\s*:\s*\{/g.exec(viteCode);
968
+ const returnHeader = /\breturn\s*\{/g.exec(viteCode);
969
+ const newline = viteContent.includes('\r\n') ? '\r\n' : '\n';
970
+ const header = aliasHeader || resolveHeader || returnHeader;
971
+ if (!header || (!aliasHeader && /\balias\s*:/.test(viteCode))) {
972
+ throw new Error('❌ Cannot safely insert @saveGallery in this Vite config. Add a resolve.alias object first.');
973
+ }
974
+ const insert = aliasHeader ? `${newline} ${aliasEntry},`
975
+ : resolveHeader ? `${newline} alias: { ${aliasEntry} },`
976
+ : `${newline} resolve: { alias: { ${aliasEntry} } },`;
977
+ const offset = header.index + header[0].length;
978
+ nextViteContent = viteContent.slice(0, offset) + insert + viteContent.slice(offset);
979
+ }
980
+ nextViteContent = ensureImportOrRequire(nextViteContent, 'path', "import path from 'path';", "const path = require('path');");
981
+ if (nextViteContent !== viteContent) {
982
+ pendingChanges.unshift({ filePath: viteConfigPath, content: viteContent, nextContent: nextViteContent,
983
+ replacements: [{ importPath: aliasMatch ? aliasMatch[3] : '(missing @saveGallery alias)', nextImportPath: targetRelativePath }] });
984
+ }
985
+ if (isIOSBuild) iosImportFound = true;
986
+ else androidImportFound = true;
987
+ }
988
+
989
+ if (pendingChanges.length) {
990
+ console.warn(`\n⚠️ WARNING: This build is considered ${isIOSBuild ? 'iOS' : 'Android'}.`);
991
+ console.warn('The following source imports and Vite alias targets will be changed:');
992
+ for (const change of pendingChanges) {
993
+ console.warn(`\n📄 ${change.filePath}`);
994
+ for (const replacement of change.replacements) {
995
+ console.warn(` ${replacement.importPath} → ${replacement.nextImportPath}`);
996
+ }
997
+ }
998
+
999
+ const confirmed = await new Promise(resolve => {
1000
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1001
+ rl.on('close', () => resolve(false));
1002
+ rl.on('SIGINT', () => { resolve(false); rl.close(); });
1003
+ rl.question('\nPress Enter or type y to apply these changes and continue; any other answer cancels: ', answer => {
1004
+ const normalizedAnswer = answer.trim().toLowerCase();
1005
+ resolve(normalizedAnswer === '' || normalizedAnswer === 'y');
1006
+ rl.close();
1007
+ });
1008
+ });
1009
+ if (!confirmed) throw new Error('❌ Build cancelled. No import filenames were changed.');
1010
+
1011
+ // Check every source again before writing so edits made while awaiting confirmation are preserved.
1012
+ for (const change of pendingChanges) {
1013
+ if (fs.readFileSync(change.filePath, 'utf8') !== change.content) {
1014
+ throw new Error(`❌ Source changed while awaiting confirmation: ${change.filePath}. Build cancelled; run it again.`);
1015
+ }
1016
+ }
1017
+ for (const change of pendingChanges) {
1018
+ fs.writeFileSync(change.filePath, change.nextContent, 'utf8');
1019
+ console.log(`✅ Updated platform imports in: ${change.filePath}`);
1020
+ }
1021
+ }
1022
+
1023
+ // The alias selects exactly one helper for the target platform.
1024
+ if (iosImportFound) {
1025
+ console.log('✅ iOS import detected (valid for iOS build).');
1026
+ } else if (androidImportFound) {
1027
+ console.log('✅ Android import detected (valid for Android build).');
1028
+ } else {
1029
+ console.log('✅ No platform-specific saveToGalleryAndSaveAnyFile imports found.');
1030
+ }
1031
+
1032
+
1033
+ // Android Checks
1034
+ if (buildPlatform === 'android' && androidImportFound) {
1035
+ console.log("📱 Android version of saveToGalleryAndSaveAnyFile detected. Checking AndroidManifest.xml...");
1036
+
1037
+ if (!fs.existsSync(ANDROID_MANIFEST_PATH)) {
1038
+ console.error("❌ AndroidManifest.xml not found. Cannot add requestLegacyExternalStorage attribute.");
1039
+ return;
1040
+ }
1041
+
1042
+ let manifestContent = fs.readFileSync(ANDROID_MANIFEST_PATH, 'utf8');
1043
+
1044
+ if (!manifestContent.includes('android:requestLegacyExternalStorage="true"')) {
1045
+ console.log("Adding android:requestLegacyExternalStorage=\"true\" to <application> tag...");
1046
+
1047
+ manifestContent = manifestContent.replace(
1048
+ /<application([^>]*)>/,
1049
+ (match, attrs) => {
1050
+ if (attrs.includes('android:requestLegacyExternalStorage')) return match;
1051
+ return `<application${attrs} android:requestLegacyExternalStorage="true">`;
1052
+ }
1053
+ );
1054
+
1055
+ fs.writeFileSync(ANDROID_MANIFEST_PATH, manifestContent, 'utf8');
1056
+ console.log("✅ android:requestLegacyExternalStorage=\"true\" added successfully.");
1057
+ } else {
1058
+ console.log("ℹ️ android:requestLegacyExternalStorage already exists in AndroidManifest.xml.");
1059
+ }
1060
+ } else {
1061
+ console.log("✅ No Android saveToGalleryAndSaveAnyFile imports detected.");
1062
+ }
1063
+ };
1064
+
1065
+ await saveToGalleryAndSaveFileCheck_iOS();
1066
+ // Validate saveToGalleryAndSaveAnyFile imports for the build platform END
1067
+
1068
+
1069
+
1070
+
1071
+
1072
+
1073
+
1074
+
1075
+
1076
+
1077
+
1078
+
1079
+
1080
+ /*
1081
+ // Clean up AppleDouble files (._*) created by macOS START
1082
+ if (process.platform === 'darwin') {
1083
+ try {
1084
+ console.log('🧹 Cleaning up AppleDouble files (._*)...');
1085
+ execSync(`find . -name '._*' -delete`);
1086
+ console.log('✅ AppleDouble files removed.');
1087
+ } catch (err) {
1088
+ console.warn('⚠️ Failed to remove AppleDouble files:', err.message);
1089
+ }
1090
+ } else {
1091
+ console.log('ℹ️ Skipping AppleDouble cleanup — not a macOS machine.');
1092
+ }
1093
+
1094
+ // Clean up AppleDouble files (._*) created by macOS END
1095
+ */
1096
+
1097
+
1098
+
1099
+
1100
+
1101
+
1102
+ //In routes.js file check static import START
1103
+
1104
+ const routesPath = path.join(process.cwd(), 'src', 'js', 'routes.js');
1105
+ const routesContent = fs.readFileSync(routesPath, 'utf-8');
1106
+
1107
+ let inBlockComment = false;
1108
+ const lines = routesContent.split('\n');
1109
+
1110
+ const allowedImport = `import HomePage from '../pages/home.f7';`;
1111
+ const badImportRegex = /^[ \t]*import\s+[\w{}*,\s]*\s+from\s+['"].+\.f7['"]\s*;/;
1112
+ const badImports = [];
1113
+
1114
+ lines.forEach((line, index) => {
1115
+ const trimmed = line.trim();
1116
+
1117
+ // Handle block comment start and end
1118
+ if (trimmed.startsWith('/*')) inBlockComment = true;
1119
+ if (inBlockComment && trimmed.endsWith('*/')) {
1120
+ inBlockComment = false;
1121
+ return;
1122
+ }
1123
+
1124
+ // Skip if inside block comment or line comment
1125
+ if (inBlockComment || trimmed.startsWith('//')) return;
1126
+
1127
+ // Match static .f7 import
1128
+ if (badImportRegex.test(trimmed) && trimmed !== allowedImport) {
1129
+ badImports.push({ line: trimmed, number: index + 1 });
1130
+ }
1131
+ });
1132
+
1133
+ if (badImports.length > 0) {
1134
+ console.error('\n❌ ERROR: Detected disallowed static imports of .f7 files in routes.js\n');
1135
+ console.error(`⚠️ Only this static import is allowed:\n ${allowedImport}\n`);
1136
+ console.error(`🔧 Please convert other imports to async dynamic imports like this:\n`);
1137
+ console.error(`
1138
+
1139
+ import HomePage from '../pages/home.f7';
1140
+
1141
+ const routes = [
1142
+ {
1143
+ path: '/',
1144
+ component:HomePage,
1145
+ },
1146
+ {
1147
+ path: '/ProfilePage/',
1148
+ async async({ resolve }) {
1149
+ const page = await import('../pages/profile.f7');
1150
+ resolve({ component: page.default });
1151
+ },
1152
+ }]
1153
+ `);
1154
+
1155
+ badImports.forEach(({ line, number }) => {
1156
+ console.error(`${number}: ${line}`);
1157
+ });
1158
+
1159
+ process.exit(1);
1160
+ } else {
1161
+ console.log('✅ routes.js passed the .f7 import check.');
1162
+ }
1163
+
1164
+ //In routes.js file check static import END
1165
+
1166
+
1167
+
1168
+
1169
+
1170
+
1171
+
1172
+
1173
+
1174
+
1175
+
1176
+
1177
+
1178
+ // Check and change the "BridgeWebViewClient.java" file START
1179
+ /*
1180
+ For crash issue due to low memory problem, we need to modify the onRenderProcessGone method in BridgeWebViewClient.java.
1181
+ */
1182
+
1183
+
1184
+ const bridgeWebViewClientFilePath = path.join(process.cwd(), 'node_modules', '@capacitor/android/capacitor/src/main/java/com/getcapacitor', 'BridgeWebViewClient.java');
1185
+
1186
+ // Read the file
1187
+ if (!fs.existsSync(bridgeWebViewClientFilePath)) {
1188
+ console.error('❌ Error: BridgeWebViewClient.java not found.');
1189
+ process.exit(1);
1190
+ }
1191
+
1192
+ let fileContent = fs.readFileSync(bridgeWebViewClientFilePath, 'utf8');
1193
+ const bridgeWebViewLineEnding = fileContent.includes('\r\n') ? '\r\n' : '\n';
1194
+ fileContent = fileContent.replace(/\r\n/g, '\n');
1195
+
1196
+ // Define old and new code
1197
+ const oldCodeStart = `@Override
1198
+ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
1199
+ super.onRenderProcessGone(view, detail);
1200
+ boolean result = false;
1201
+
1202
+ List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
1203
+ if (webViewListeners != null) {
1204
+ for (WebViewListener listener : bridge.getWebViewListeners()) {
1205
+ result = listener.onRenderProcessGone(view, detail) || result;
1206
+ }
1207
+ }
1208
+
1209
+ return result;
1210
+ }`;
1211
+
1212
+ // Match the previous patch so existing projects migrate without reinstalling Capacitor.
1213
+ const legacyReloadCode = `@Override
1214
+ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
1215
+ super.onRenderProcessGone(view, detail);
1216
+
1217
+ boolean result = false;
1218
+
1219
+ List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
1220
+ if (webViewListeners != null) {
1221
+ for (WebViewListener listener : bridge.getWebViewListeners()) {
1222
+ result = listener.onRenderProcessGone(view, detail) || result;
1223
+ }
1224
+ }
1225
+
1226
+ if (!result) {
1227
+ // If no one handled it, handle it ourselves!
1228
+
1229
+ /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1230
+ if (detail.didCrash()) {
1231
+ //Log.e("CapacitorWebView", "WebView crashed internally!");
1232
+ } else {
1233
+ //Log.w("CapacitorWebView", "WebView was killed by system (low memory) internally!");
1234
+ }
1235
+ }*/
1236
+
1237
+ view.post(() -> {
1238
+ Toast.makeText(view.getContext(), "Reloading due to low memory issue", Toast.LENGTH_SHORT).show();
1239
+ });
1240
+
1241
+ view.reload(); // Safely reload WebView
1242
+
1243
+ return true; // We handled it
1244
+ }
1245
+
1246
+ return result;
1247
+ }`;
1248
+
1249
+ const newCode = `// Codeplay WebView renderer recovery (before-build 6.5).
1250
+ private boolean codeplayRendererRecoveryPending = false;
1251
+ private static long codeplayRendererRecoveryWindowStart = -1L;
1252
+ private static int codeplayRendererRecoveryCount = 0;
1253
+
1254
+ @Override
1255
+ public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
1256
+ if (codeplayRendererRecoveryPending) return true;
1257
+
1258
+ boolean handled = false;
1259
+ List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
1260
+ if (webViewListeners != null) {
1261
+ for (WebViewListener listener : webViewListeners) {
1262
+ handled = listener.onRenderProcessGone(view, detail) || handled;
1263
+ }
1264
+ }
1265
+ if (handled) return true;
1266
+
1267
+ codeplayRendererRecoveryPending = true;
1268
+ final androidx.appcompat.app.AppCompatActivity activity = bridge.getActivity();
1269
+ final boolean wasInForeground = activity != null && activity.getLifecycle()
1270
+ .getCurrentState().isAtLeast(androidx.lifecycle.Lifecycle.State.STARTED);
1271
+
1272
+ // A terminated renderer's WebView must never be loaded or reloaded again.
1273
+ if (view.getParent() instanceof android.view.ViewGroup) {
1274
+ ((android.view.ViewGroup) view.getParent()).removeView(view);
1275
+ }
1276
+ view.destroy();
1277
+
1278
+ if (activity == null || activity.isFinishing() || activity.isDestroyed()) return true;
1279
+
1280
+ final android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
1281
+ final Runnable recover = () -> {
1282
+ if (activity.isFinishing() || activity.isDestroyed()) return;
1283
+
1284
+ // Static counters survive Activity recreation and prevent an automatic restart loop.
1285
+ long now = android.os.SystemClock.elapsedRealtime();
1286
+ if (codeplayRendererRecoveryWindowStart < 0L ||
1287
+ now - codeplayRendererRecoveryWindowStart >= 60000L) {
1288
+ codeplayRendererRecoveryWindowStart = now;
1289
+ codeplayRendererRecoveryCount = 0;
1290
+ }
1291
+
1292
+ if (codeplayRendererRecoveryCount >= 3) {
1293
+ new android.app.AlertDialog.Builder(activity)
1294
+ .setTitle("Unable to restore the app")
1295
+ .setMessage("The app could not recover automatically. Please try again.")
1296
+ .setCancelable(false)
1297
+ .setPositiveButton("Try again", (dialog, which) -> {
1298
+ codeplayRendererRecoveryWindowStart = -1L;
1299
+ codeplayRendererRecoveryCount = 0;
1300
+ if (!activity.isFinishing() && !activity.isDestroyed()) activity.recreate();
1301
+ })
1302
+ .setNegativeButton("Close", (dialog, which) -> activity.finish())
1303
+ .show();
1304
+ return;
1305
+ }
1306
+
1307
+ codeplayRendererRecoveryCount++;
1308
+ if (!wasInForeground) {
1309
+ Toast.makeText(activity.getApplicationContext(), "Restoring your app…", Toast.LENGTH_SHORT).show();
1310
+ }
1311
+ activity.recreate();
1312
+ };
1313
+
1314
+ if (wasInForeground) {
1315
+ mainHandler.post(recover);
1316
+ } else {
1317
+ // Do not allocate another renderer while the app is hidden and memory may still be low.
1318
+ activity.getLifecycle().addObserver(new androidx.lifecycle.LifecycleEventObserver() {
1319
+ @Override
1320
+ public void onStateChanged(
1321
+ androidx.lifecycle.LifecycleOwner source,
1322
+ androidx.lifecycle.Lifecycle.Event event
1323
+ ) {
1324
+ if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) {
1325
+ source.getLifecycle().removeObserver(this);
1326
+ mainHandler.post(recover);
1327
+ } else if (event == androidx.lifecycle.Lifecycle.Event.ON_DESTROY) {
1328
+ source.getLifecycle().removeObserver(this);
1329
+ }
1330
+ }
1331
+ });
1332
+ }
1333
+ return true;
1334
+ }`;
1335
+
1336
+ // Step 1: Update the stock method or migrate the previous reload-based patch.
1337
+ let updated = false;
1338
+
1339
+ if (fileContent.includes(newCode)) {
1340
+ console.log('ℹ️ Method already updated. No changes needed in "BridgeWebViewClient.java".');
1341
+ } else if (fileContent.includes(legacyReloadCode)) {
1342
+ fileContent = fileContent.replace(legacyReloadCode, newCode);
1343
+ updated = true;
1344
+ } else if (fileContent.includes(oldCodeStart)) {
1345
+ fileContent = fileContent.replace(oldCodeStart, newCode);
1346
+ updated = true;
1347
+ } else {
1348
+ console.error('❌ Error: Neither old nor new code found. Unexpected content.');
1349
+ process.exit(1);
1350
+ }
1351
+
1352
+ // Step 2: Check and add import if missing
1353
+ const importToast = 'import android.widget.Toast;';
1354
+ if (!fileContent.includes(importToast)) {
1355
+ console.log('✅ Adding missing import for Toast...');
1356
+ const importRegex = /import\s+[^;]+;/g;
1357
+ const matches = [...fileContent.matchAll(importRegex)];
1358
+
1359
+ if (matches.length > 0) {
1360
+ const lastImport = matches[matches.length - 1];
1361
+ const insertPosition = lastImport.index + lastImport[0].length;
1362
+ fileContent = fileContent.slice(0, insertPosition) + `\n${importToast}` + fileContent.slice(insertPosition);
1363
+ updated = true;
1364
+ } else {
1365
+ console.error('❌ Error: No import section found in file.');
1366
+ process.exit(1);
1367
+ }
1368
+ } else {
1369
+ console.log('ℹ️ Import for Toast already exists. No changes needed.');
1370
+ }
1371
+
1372
+ // Step 3: Save if updated
1373
+ if (updated) {
1374
+ fs.writeFileSync(bridgeWebViewClientFilePath, fileContent.replace(/\n/g, bridgeWebViewLineEnding), 'utf8');
1375
+ console.log('✅ File updated successfully.');
1376
+ } else {
1377
+ console.log('ℹ️ No changes needed.');
1378
+ }
1379
+
1380
+
1381
+
1382
+
1383
+ // Check and change the "BridgeWebViewClient.java" file END
1384
+
1385
+
1386
+
1387
+
1388
+
1389
+
1390
+
1391
+
1392
+ /*
1393
+ // To resolve the kotlin version issue, we need to update the kotlin version in the build.gradle file START
1394
+
1395
+ // Build the path dynamically like you requested
1396
+ const gradlePath = path.join(
1397
+ process.cwd(),
1398
+ 'android',
1399
+ 'build.gradle'
1400
+ );
1401
+
1402
+ // Read the existing build.gradle
1403
+ let gradleContent = fs.readFileSync(gradlePath, 'utf8');
1404
+
1405
+ // Add `ext.kotlin_version` if it's not already there
1406
+ if (!gradleContent.includes('ext.kotlin_version')) {
1407
+ gradleContent = gradleContent.replace(
1408
+ /buildscript\s*{/,
1409
+ `buildscript {\n ext.kotlin_version = '2.1.0'`
1410
+ );
1411
+ }
1412
+
1413
+ // Add Kotlin classpath if it's not already there
1414
+ if (!gradleContent.includes('org.jetbrains.kotlin:kotlin-gradle-plugin')) {
1415
+ gradleContent = gradleContent.replace(
1416
+ /dependencies\s*{([\s\S]*?)classpath 'com.android.tools.build:gradle:8.7.2'/,
1417
+ `dependencies {\n classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")\n$1classpath 'com.android.tools.build:gradle:8.7.2'`
1418
+ );
1419
+ }
1420
+
1421
+ // Write back the modified content
1422
+ fs.writeFileSync(gradlePath, gradleContent, 'utf8');
1423
+
1424
+ console.log('✅ Kotlin version updated in build.gradle.');
1425
+
1426
+ // To resolve the kotlin version issue, we need to update the kotlin version in the build.gradle file END
1427
+ */
1428
+
1429
+
1430
+
1431
+
1432
+
1433
+
1434
+
1435
+
1436
+ let _admobConfig;
1437
+
1438
+
1439
+
1440
+ const androidPlatformPath = path.join(process.cwd(), 'android');
1441
+ const iosPlatformPath = path.join(process.cwd(), 'ios');
1442
+ const androidManifestPath = path.join(process.cwd(), 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
1443
+ const androidAppBuildGradlePath = path.join(process.cwd(), 'android', 'app', 'build.gradle');
1444
+ const AMAZON_WINDOW_EXTENSIONS_DEPENDENCY = 'implementation "androidx.window.extensions.core:core:1.0.0"';
1445
+ const AMAZON_WINDOW_EXTENSIONS_DEPENDENCY_PATTERN = /^[ \t]*implementation\s+["']androidx\.window\.extensions\.core:core:[^"']+["'][ \t]*(?:\r?\n)?/gm;
1446
+ const infoPlistPath = path.join(process.cwd(), 'ios', 'App', 'App', 'Info.plist');
1447
+ const iosSceneDelegatePath = path.join(process.cwd(), 'ios', 'App', 'App', 'SceneDelegate.swift');
1448
+ const resourcesPath = path.join(process.cwd(), 'resources', 'res');
1449
+ const androidResPath = path.join(process.cwd(), 'android', 'app', 'src', 'main', 'res');
1450
+ const localNotificationsPluginPath = path.join(process.cwd(), 'node_modules', '@capacitor', 'local-notifications');
1451
+
1452
+ function fileExists(filePath) {
1453
+ return fs.existsSync(filePath);
1454
+ }
1455
+
1456
+ function copyFolderSync(source, target) {
1457
+ if (!fs.existsSync(target)) {
1458
+ fs.mkdirSync(target, { recursive: true });
1459
+ }
1460
+
1461
+ fs.readdirSync(source).forEach(file => {
1462
+ const sourceFile = path.join(source, file);
1463
+ const targetFile = path.join(target, file);
1464
+
1465
+ if (fs.lstatSync(sourceFile).isDirectory()) {
1466
+ copyFolderSync(sourceFile, targetFile);
1467
+ } else {
1468
+ fs.copyFileSync(sourceFile, targetFile);
1469
+ }
1470
+ });
1471
+ }
1472
+
1473
+ function checkAndCopyResources() {
1474
+ if (fileExists(resourcesPath)) {
1475
+ copyFolderSync(resourcesPath, androidResPath);
1476
+ console.log('✅ Successfully copied resources/res to android/app/src/main/res.');
1477
+ } else {
1478
+ console.log('resources/res folder not found.');
1479
+
1480
+ if (fileExists(localNotificationsPluginPath)) {
1481
+ throw new Error('❌ resources/res is required for @capacitor/local-notifications. Stopping execution.');
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+
1487
+
1488
+
1489
+
1490
+
1491
+
1492
+
1493
+ function getAdMobConfig() {
1494
+ if (!fileExists(configPath)) {
1495
+ throw new Error('❌ capacitor.config.json not found. Ensure this is a Capacitor project.');
1496
+ }
1497
+
1498
+ const capacitorConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1499
+ const packageJson = fileExists(packageJsonPath)
1500
+ ? JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
1501
+ : {};
1502
+
1503
+ const legacyAdmobConfig = capacitorConfig.plugins?.AdMob || {};
1504
+ const nextGenAdmobConfig = packageJson.admob || {};
1505
+ const admobJsonAppIds = getAdmobJsonAppIds();
1506
+
1507
+ if (!legacyAdmobConfig && !nextGenAdmobConfig) {
1508
+ throw new Error('❌ AdMob configuration is missing in package.json or capacitor.config.json.');
1509
+ }
1510
+
1511
+ const isEnabled =
1512
+ nextGenAdmobConfig.enabled !== false &&
1513
+ legacyAdmobConfig.ADMOB_ENABLED !== false;
1514
+
1515
+ if (!isEnabled) {
1516
+ return { ADMOB_ENABLED: false };
1517
+ }
1518
+
1519
+ const androidAppId = admobJsonAppIds.androidAppId || nextGenAdmobConfig.androidAppId || legacyAdmobConfig.APP_ID_ANDROID;
1520
+ const iosAppId = admobJsonAppIds.iosAppId || nextGenAdmobConfig.iosAppId || legacyAdmobConfig.APP_ID_IOS;
1521
+ const userTrackingDescription =
1522
+ nextGenAdmobConfig.userTrackingDescription ||
1523
+ legacyAdmobConfig.USER_TRACKING_DESCRIPTION ||
1524
+ 'This identifier will be used to deliver personalized ads to you.';
1525
+
1526
+ if (!androidAppId || !iosAppId) {
1527
+ throw new Error(' ❌ AdMob configuration is incomplete. Ensure androidAppId/iosAppId or APP_ID_ANDROID/APP_ID_IOS are defined.');
1528
+ }
1529
+
1530
+ return {
1531
+ ADMOB_ENABLED: true,
1532
+ APP_ID_ANDROID: androidAppId,
1533
+ APP_ID_IOS: iosAppId,
1534
+ USER_TRACKING_DESCRIPTION: userTrackingDescription,
1535
+ USE_LITE_ADS: legacyAdmobConfig.USE_LITE_ADS === "lite",
1536
+ };
1537
+ }
1538
+
1539
+ function syncNextGenAdmobPackageJsonFromCapacitorConfig() {
1540
+ if (!fileExists(configPath) || !fileExists(packageJsonPath)) {
1541
+ return;
1542
+ }
1543
+
1544
+ const capacitorConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1545
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1546
+
1547
+ const legacyAdmobConfig = capacitorConfig.plugins?.AdMob || {};
1548
+ const admobJsonAppIds = getAdmobJsonAppIds();
1549
+ const androidAppId = admobJsonAppIds.androidAppId || legacyAdmobConfig.APP_ID_ANDROID;
1550
+ const iosAppId = admobJsonAppIds.iosAppId || legacyAdmobConfig.APP_ID_IOS;
1551
+
1552
+ if (!androidAppId || !iosAppId) {
1553
+ return;
1554
+ }
1555
+
1556
+ const desiredAdmobConfig = {
1557
+ androidAppId,
1558
+ iosAppId,
1559
+ enableNativeAds: {
1560
+ ios: packageJson.admob?.enableNativeAds?.ios ?? true,
1561
+ android: packageJson.admob?.enableNativeAds?.android ?? true,
1562
+ },
1563
+ userTrackingDescription:
1564
+ packageJson.admob?.userTrackingDescription ||
1565
+ 'This identifier will be used to deliver personalized ads to you.',
1566
+ };
1567
+
1568
+ const existingAdmobConfig = packageJson.admob || {};
1569
+ const mergedAdmobConfig = { ...existingAdmobConfig };
1570
+ let updated = false;
1571
+
1572
+ const syncKeyByKey = (target, source) => {
1573
+ for (const [key, value] of Object.entries(source)) {
1574
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1575
+ if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
1576
+ target[key] = {};
1577
+ updated = true;
1578
+ }
1579
+
1580
+ syncKeyByKey(target[key], value);
1581
+ continue;
1582
+ }
1583
+
1584
+ if (target[key] !== value) {
1585
+ target[key] = value;
1586
+ updated = true;
1587
+ }
1588
+ }
1589
+ };
1590
+
1591
+ syncKeyByKey(mergedAdmobConfig, desiredAdmobConfig);
1592
+
1593
+ if (!updated) {
1594
+ return;
1595
+ }
1596
+
1597
+ packageJson.admob = mergedAdmobConfig;
1598
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
1599
+ console.log(admobJsonAppIds.androidAppId || admobJsonAppIds.iosAppId
1600
+ ? '✅ package.json AdMob settings synced from AdMob configuration JSON'
1601
+ : '✅ package.json AdMob settings synced from capacitor.config.json');
1602
+ }
1603
+
1604
+ function validateAndroidBuildOptions() {
1605
+
1606
+
1607
+ if (!fileExists(configPath)) {
1608
+ console.log('❌ capacitor.config.json not found. Ensure this is a Capacitor project.');
1609
+ process.exit(1);
1610
+ }
1611
+
1612
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1613
+
1614
+ const targetAppId=config.appId
1615
+
1616
+ const buildOptions = config.android?.buildOptions;
1617
+
1618
+ if (!buildOptions) {
1619
+ console.log('❌ Missing android.buildOptions in capacitor.config.json.');
1620
+ process.exit(1);
1621
+ }
1622
+
1623
+ const requiredProps = [
1624
+ 'keystorePath',
1625
+ 'keystorePassword',
1626
+ 'keystoreAlias',
1627
+ 'keystoreAliasPassword',
1628
+ 'releaseType',
1629
+ 'signingType'
1630
+ ];
1631
+
1632
+ const missing = requiredProps.filter(prop => !buildOptions[prop]);
1633
+
1634
+ if (missing.length > 0) {
1635
+ console.log('❌ Missing properties android.buildOptions in capacitor.config.json.');
1636
+ process.exit(1);
1637
+ }
1638
+
1639
+
1640
+ const keystorePath=buildOptions.keystorePath
1641
+ const keyFileName = path.basename(keystorePath);
1642
+
1643
+
1644
+
1645
+ const keystoreMap = {
1646
+ "gameskey.jks": [
1647
+ "com.cube.blaster",
1648
+ ],
1649
+ "htmleditorkeystoke.jks": [
1650
+ "com.HTML.AngularJS.Codeplay",
1651
+ "com.html.codeplay.pro",
1652
+ "com.bootstrap.code.play",
1653
+ "com.kids.learning.master",
1654
+ "com.Simple.Barcode.Scanner",
1655
+ "com.FAShtmlcssjs.editor"
1656
+ ]
1657
+ };
1658
+
1659
+ // find which keystore is required for the given targetAppId
1660
+ let requiredKey = "newappskey.jks"; // default
1661
+ for (const [keyFile, appIds] of Object.entries(keystoreMap)) {
1662
+ if (appIds.includes(targetAppId)) {
1663
+ requiredKey = keyFile;
1664
+ break;
1665
+ }
1666
+ }
1667
+
1668
+ // validate
1669
+ if (keyFileName !== requiredKey) {
1670
+ console.log(`❌ The keystore path is mismatched. Expected ${requiredKey} for ${targetAppId}, but got ${keyFileName}`);
1671
+ process.exit(1);
1672
+ }
1673
+
1674
+
1675
+
1676
+
1677
+
1678
+ // optionally return them
1679
+ //return buildOptions;
1680
+ }
1681
+
1682
+ function updateAndroidManifest(admobConfig) {
1683
+ if (!fileExists(androidManifestPath)) {
1684
+ console.error(' ❌ AndroidManifest.xml not found. Ensure the Android platform is added.');
1685
+ return;
1686
+ }
1687
+
1688
+ let manifestContent = fs.readFileSync(androidManifestPath, 'utf8');
1689
+ const metaDataTag = `<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="${admobConfig.APP_ID_ANDROID}" />`;
1690
+
1691
+ if (/com\.google\.android\.gms\.ads\.APPLICATION_ID/.test(manifestContent)) {
1692
+ manifestContent = manifestContent.replace(
1693
+ /<meta-data\s+android:name="com\.google\.android\.gms\.ads\.APPLICATION_ID"\s+android:value=".*?"\s*\/>/,
1694
+ metaDataTag
1695
+ );
1696
+ } else {
1697
+ manifestContent = manifestContent.replace(
1698
+ /<application\b([^>]*)>/,
1699
+ `<application$1>\n ${metaDataTag}`
1700
+ );
1701
+ }
1702
+
1703
+ fs.writeFileSync(androidManifestPath, manifestContent, 'utf8');
1704
+ console.log('✅ AdMob Android App ID successfully updated in AndroidManifest.xml');
1705
+ }
1706
+
1707
+ function updateInfoPlist(admobConfig) {
1708
+ if (!fileExists(infoPlistPath)) {
1709
+ console.error('Info.plist not found. Ensure you have built the iOS project.');
1710
+ return;
1711
+ }
1712
+
1713
+ const plistContent = fs.readFileSync(infoPlistPath, 'utf8');
1714
+ const plistData = plist.parse(plistContent);
1715
+
1716
+ plistData.GADApplicationIdentifier = admobConfig.APP_ID_IOS;
1717
+ if (isKidsAdmobApp()) delete plistData.NSUserTrackingUsageDescription;
1718
+ else plistData.NSUserTrackingUsageDescription = admobConfig.USER_TRACKING_DESCRIPTION;
1719
+ plistData.GADDelayAppMeasurementInit = true;
1720
+
1721
+ // https://developers.google.com/admob/ios/quick-start
1722
+ plistData.SKAdNetworkItems = [
1723
+ { SKAdNetworkIdentifier: 'cstr6suwn9.skadnetwork' }, // Google
1724
+ { SKAdNetworkIdentifier: '4fzdc2evr5.skadnetwork' }, // Aarki
1725
+ { SKAdNetworkIdentifier: '2fnua5tdw4.skadnetwork' }, // Adform
1726
+ { SKAdNetworkIdentifier: 'ydx93a7ass.skadnetwork' }, // Adikteev
1727
+ { SKAdNetworkIdentifier: 'p78axxw29g.skadnetwork' }, // Amazon
1728
+ { SKAdNetworkIdentifier: 'v72qych5uu.skadnetwork' }, // Appier
1729
+ { SKAdNetworkIdentifier: 'ludvb6z3bs.skadnetwork' }, // Applovin
1730
+ { SKAdNetworkIdentifier: 'cp8zw746q7.skadnetwork' }, // Arpeely
1731
+ { SKAdNetworkIdentifier: '3sh42y64q3.skadnetwork' }, // Basis
1732
+ { SKAdNetworkIdentifier: 'c6k4g5qg8m.skadnetwork' }, // Beeswax.io
1733
+ { SKAdNetworkIdentifier: 's39g8k73mm.skadnetwork' }, // Bidease
1734
+ { SKAdNetworkIdentifier: '3qy4746246.skadnetwork' }, // Bigabid
1735
+ { SKAdNetworkIdentifier: 'hs6bdukanm.skadnetwork' }, // Criteo
1736
+ { SKAdNetworkIdentifier: 'mlmmfzh3r3.skadnetwork' }, // Digital Turbine DSP
1737
+ { SKAdNetworkIdentifier: 'v4nxqhlyqp.skadnetwork' }, // i-mobile
1738
+ { SKAdNetworkIdentifier: 'wzmmz9fp6w.skadnetwork' }, // InMobi
1739
+ { SKAdNetworkIdentifier: 'su67r6k2v3.skadnetwork' }, // ironSource Ads
1740
+ { SKAdNetworkIdentifier: 'yclnxrl5pm.skadnetwork' }, // Jampp
1741
+ { SKAdNetworkIdentifier: '7ug5zh24hu.skadnetwork' }, // Liftoff
1742
+ { SKAdNetworkIdentifier: 'gta9lk7p23.skadnetwork' }, // Liftoff Monetize
1743
+ { SKAdNetworkIdentifier: 'vutu7akeur.skadnetwork' }, // LINE
1744
+ { SKAdNetworkIdentifier: 'y5ghdn5j9k.skadnetwork' }, // Mediaforce
1745
+ { SKAdNetworkIdentifier: 'v9wttpbfk9.skadnetwork' }, // Meta (1 of 2)
1746
+ { SKAdNetworkIdentifier: 'n38lu8286q.skadnetwork' }, // Meta (2 of 2)
1747
+ { SKAdNetworkIdentifier: '47vhws6wlr.skadnetwork' }, // MicroAd
1748
+ { SKAdNetworkIdentifier: 'kbd757ywx3.skadnetwork' }, // Mintegral / Mobvista
1749
+ { SKAdNetworkIdentifier: '9t245vhmpl.skadnetwork' }, // Moloco
1750
+ { SKAdNetworkIdentifier: 'a2p9lx4jpn.skadnetwork' }, // Opera
1751
+ { SKAdNetworkIdentifier: '22mmun2rn5.skadnetwork' }, // Pangle
1752
+ { SKAdNetworkIdentifier: '4468km3ulz.skadnetwork' }, // Realtime Technologies GmbH
1753
+ { SKAdNetworkIdentifier: '2u9pt9hc89.skadnetwork' }, // Remerge
1754
+ { SKAdNetworkIdentifier: '8s468mfl3y.skadnetwork' }, // RTB House
1755
+ { SKAdNetworkIdentifier: 'ppxm28t8ap.skadnetwork' }, // Smadex
1756
+ { SKAdNetworkIdentifier: 'uw77j35x4d.skadnetwork' }, // The Trade Desk
1757
+ { SKAdNetworkIdentifier: 'pwa73g5rt2.skadnetwork' }, // Tremor
1758
+ { SKAdNetworkIdentifier: '578prtvx9j.skadnetwork' }, // Unicorn
1759
+ { SKAdNetworkIdentifier: '4dzt52r2t5.skadnetwork' }, // Unity Ads
1760
+ { SKAdNetworkIdentifier: 'tl55sbb4fm.skadnetwork' }, // Verve
1761
+ { SKAdNetworkIdentifier: 'e5fvkxwrpn.skadnetwork' }, // Yahoo!
1762
+ { SKAdNetworkIdentifier: '8c4e2ghe7u.skadnetwork' }, // Yahoo! Japan Ads
1763
+ { SKAdNetworkIdentifier: '3rd42ekr43.skadnetwork' }, // YouAppi
1764
+ { SKAdNetworkIdentifier: '3qcr597p9d.skadnetwork' }, // Zucks
1765
+ ];
1766
+
1767
+ const updatedPlistContent = plist.build(plistData);
1768
+ fs.writeFileSync(infoPlistPath, updatedPlistContent, 'utf8');
1769
+ console.log('AdMob IDs and additional configurations successfully updated in Info.plist');
1770
+ }
1771
+
1772
+ function updateIOSSceneDelegateForATT() {
1773
+ if (!fileExists(iosSceneDelegatePath)) {
1774
+ console.warn('SceneDelegate.swift not found. Skipping the iOS ATT scene update.');
1775
+ return;
1776
+ }
1777
+
1778
+ let content = fs.readFileSync(iosSceneDelegatePath, 'utf8');
1779
+ const newline = content.includes('\r\n') ? '\r\n' : '\n';
1780
+ const requestBody = [
1781
+ ' if ATTrackingManager.trackingAuthorizationStatus == .notDetermined &&',
1782
+ ' !isTrackingAuthorizationRequestScheduled {',
1783
+ ' isTrackingAuthorizationRequestScheduled = true',
1784
+ '',
1785
+ ' DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in',
1786
+ ' self?.isTrackingAuthorizationRequestScheduled = false',
1787
+ '',
1788
+ ' guard UIApplication.shared.applicationState == .active,',
1789
+ ' ATTrackingManager.trackingAuthorizationStatus == .notDetermined else { return }',
1790
+ '',
1791
+ ' ATTrackingManager.requestTrackingAuthorization { _ in }',
1792
+ ' }',
1793
+ ' }',
1794
+ ].join(newline);
1795
+
1796
+ // ATT is owned by the ad bootstrap after UMP consent. Remove the block
1797
+ // injected by earlier builds so scene activation cannot race that flow.
1798
+ const originalContent = content;
1799
+ content = content.split(requestBody).join('');
1800
+ if (content.includes('ATTrackingManager.requestTrackingAuthorization')) {
1801
+ throw new Error('SceneDelegate.swift still contains a custom ATT request that can precede UMP consent. Remove that request before building.');
1802
+ }
1803
+ content = content.replace(/^[ \t]*private var isTrackingAuthorizationRequestScheduled = false\r?\n/gm, '');
1804
+ if (!content.includes('ATTrackingManager')) {
1805
+ content = content.replace(/^import AppTrackingTransparency\r?\n/gm, '');
1806
+ }
1807
+ if (content !== originalContent) fs.writeFileSync(iosSceneDelegatePath, content, 'utf8');
1808
+ console.log('iOS startup ATT prompt disabled; the ad consent flow controls ATT.');
1809
+ }
1810
+
1811
+
1812
+ try {
1813
+ if (!fileExists(configPath)) {
1814
+ throw new Error(' ❌ capacitor.config.json not found. Skipping setup.');
1815
+ }
1816
+
1817
+ if (!fileExists(androidPlatformPath) && !fileExists(iosPlatformPath)) {
1818
+ throw new Error('Neither Android nor iOS platforms are found. Ensure platforms are added to your Capacitor project.');
1819
+ }
1820
+
1821
+ checkAndCopyResources();
1822
+
1823
+ syncNextGenAdmobPackageJsonFromCapacitorConfig();
1824
+
1825
+
1826
+ _admobConfig = getAdMobConfig();
1827
+
1828
+
1829
+
1830
+
1831
+
1832
+ // Proceed only if ADMOB_ENABLED is true
1833
+ if (_admobConfig.ADMOB_ENABLED) {
1834
+ if (fileExists(androidPlatformPath)) {
1835
+ updateAndroidManifest(_admobConfig);
1836
+ }
1837
+
1838
+ if (fileExists(iosPlatformPath)) {
1839
+ updateInfoPlist(_admobConfig);
1840
+ updateIOSSceneDelegateForATT();
1841
+ }
1842
+ }
1843
+
1844
+
1845
+ } catch (error) {
1846
+ console.error(error.message);
1847
+ process.exit(1); // Stop execution if there's a critical error
1848
+ }
1849
+
1850
+
1851
+
1852
+ validateAndroidBuildOptions();
1853
+
1854
+
1855
+
1856
+
1857
+
1858
+
1859
+ // Check all the codeplays plugins version START
1860
+
1861
+
1862
+
1863
+
1864
+ //const srcDir = path.join(__dirname, 'src');
1865
+ const srcDir = path.join(process.cwd(), 'src');
1866
+ let outdatedPlugins = [];
1867
+
1868
+ function parseVersion(ver) {
1869
+ return ver.split('.').map(n => parseInt(n, 10));
1870
+ }
1871
+
1872
+ function compareVersions(v1, v2) {
1873
+ const [a1, b1] = parseVersion(v1);
1874
+ const [a2, b2] = parseVersion(v2);
1875
+ if (a1 !== a2) return a1 - a2;
1876
+ return b1 - b2;
1877
+ }
1878
+
1879
+ function walkSync(dir, filelist = []) {
1880
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1881
+ const fullPath = path.join(dir, entry.name);
1882
+ const isDirectory = entry.isDirectory() || (entry.isSymbolicLink() && fs.statSync(fullPath).isDirectory());
1883
+ if (isDirectory) walkSync(fullPath, filelist);
1884
+ else filelist.push(fullPath);
1885
+ }
1886
+ return filelist;
1887
+ }
1888
+
1889
+ function getSearchRoot(plugin) {
1890
+ return path.join(srcDir, plugin.baseDir || 'js');
1891
+ }
1892
+
1893
+
1894
+
1895
+
1896
+
1897
+
1898
+
1899
+
1900
+
1901
+
1902
+
1903
+
1904
+
1905
+
1906
+
1907
+
1908
+
1909
+
1910
+
1911
+
1912
+ /*############################################## AUTO DOWNLOAD FROM SERVER START #####################################*/
1913
+
1914
+ // ============================================================
1915
+ // 🔥 AUTO PLUGIN UPDATE SYSTEM (MANDATORY UPDATES)
1916
+ // ============================================================
1917
+
1918
+ const https = require("https");
1919
+
1920
+ /**
1921
+ * Check if file exists on server using HEAD request
1922
+ */
1923
+ function urlExists(url) {
1924
+ return new Promise(resolve => {
1925
+ const req = https.request(url, { method: "HEAD" }, res => {
1926
+ res.resume();
1927
+ resolve(res.statusCode === 200);
1928
+ });
1929
+
1930
+ req.on("error", () => resolve(false));
1931
+ req.setTimeout(8000, () => {
1932
+ req.destroy();
1933
+ resolve(false);
1934
+ });
1935
+ req.end();
1936
+ });
1937
+ }
1938
+
1939
+ /**
1940
+ * Download file from server
1941
+ */
1942
+ function downloadFile(url, dest) {
1943
+ if (path.resolve(dest).startsWith(path.join(process.cwd(), 'src') + path.sep)) sourceFileInventory = null;
1944
+ return new Promise((resolve, reject) => {
1945
+ const file = fs.createWriteStream(dest);
1946
+
1947
+ const req = https.get(url, response => {
1948
+ if (response.statusCode !== 200) {
1949
+ response.resume();
1950
+ file.close(() => fs.unlink(dest, () => {}));
1951
+ reject("Download failed");
1952
+ return;
1953
+ }
1954
+
1955
+ response.pipe(file);
1956
+
1957
+ file.on("finish", () => {
1958
+ file.close(resolve);
1959
+ });
1960
+ }).on("error", err => {
1961
+ fs.unlink(dest, () => {});
1962
+ reject(err);
1963
+ });
1964
+
1965
+ req.setTimeout(30000, () => {
1966
+ req.destroy();
1967
+ file.close(() => fs.unlink(dest, () => {}));
1968
+ reject(new Error(`Download timed out: ${url}`));
1969
+ });
1970
+ });
1971
+ }
1972
+
1973
+ /**
1974
+ * Update imports across src folder
1975
+ * Replaces old filename → new filename
1976
+ */
1977
+ /**
1978
+ * Update imports across project
1979
+ * Replaces old filename → new filename
1980
+ */
1981
+
1982
+ const VITE_ALIAS_ONLY = [
1983
+ "common",
1984
+ "admob-emi",
1985
+ "admob-emi-nextgen",
1986
+ "localization",
1987
+ "theme",
1988
+ "certificatejs",
1989
+ "ffmpeg"
1990
+ ];
1991
+
1992
+ function updateImports(oldName, newName) {
1993
+
1994
+ const projectRoot = process.cwd();
1995
+ const oldSaveHelper = oldName.match(/^(saveToGalleryAndSaveAnyFile-\d+(?:\.\d+)*)(?:-ios)?\.js$/);
1996
+ const newSaveHelper = newName.match(/^(saveToGalleryAndSaveAnyFile-\d+(?:\.\d+)*)(?:-ios)?\.js$/);
1997
+
1998
+ const filesToScan = [
1999
+ path.join(projectRoot, "vite.config.js"),
2000
+ path.join(projectRoot, "vite.config.mjs")
2001
+ ];
2002
+
2003
+ const srcDir = path.join(projectRoot, "src");
2004
+
2005
+ // scan vite config
2006
+ filesToScan.forEach(file => {
2007
+
2008
+ if (!fs.existsSync(file)) return;
2009
+
2010
+ let content = fs.readFileSync(file, "utf8");
2011
+
2012
+ if (content.includes(oldName) || (oldSaveHelper && newSaveHelper && content.includes('@saveGallery'))) {
2013
+
2014
+ content = content.split(oldName).join(newName);
2015
+ // Plugin updates can replace an iOS filename with the generic new filename.
2016
+ // Keep the centralized save-gallery alias aligned with the selected build.
2017
+ content = content.replace(/(['"]@saveGallery['"]\s*:\s*path\.resolve\(\s*__dirname\s*,\s*['"])([^'"]+)(['"]\s*\))/g,
2018
+ (match, prefix, target, suffix) => {
2019
+ if (!/saveToGalleryAndSaveAnyFile-\d+(?:\.\d+)*(?:-ios)?\.js$/.test(target)) return match;
2020
+ if (oldSaveHelper && newSaveHelper && target.includes(oldSaveHelper[1])) {
2021
+ target = target.replace(oldSaveHelper[1], newSaveHelper[1]);
2022
+ }
2023
+ const nextTarget = target.replace(/(?:-ios)?\.js$/, buildPlatform === 'ios' ? '-ios.js' : '.js');
2024
+ if (!fs.existsSync(path.resolve(path.dirname(file), nextTarget))) {
2025
+ throw new Error(`❌ Updated @saveGallery target is missing: ${nextTarget}`);
2026
+ }
2027
+ return prefix + nextTarget + suffix;
2028
+ });
2029
+
2030
+ fs.writeFileSync(file, content);
2031
+
2032
+ console.log(`✏️ Updated alias in ${path.basename(file)}`);
2033
+ }
2034
+
2035
+ });
2036
+
2037
+ // Downloads invalidate the inventory; content-only edits reuse it.
2038
+ for (const full of getSourceFiles()) {
2039
+ const relativePath = path.relative(srcDir, full);
2040
+ if (relativePath.split(path.sep).some(part => ['node_modules', 'android', 'ios', 'dist', '.git'].includes(part))) continue;
2041
+ if (!/\.(js|f7|mjs)$/.test(full) || full.endsWith('.min.js')) continue;
2042
+ let content = fs.readFileSync(full, 'utf8');
2043
+ if (!content.includes(oldName)) continue;
2044
+ content = content.split(oldName).join(newName);
2045
+ fs.writeFileSync(full, content);
2046
+ console.log(`✏️ Updated import in ${path.relative(projectRoot, full)}`);
2047
+ }
2048
+
2049
+ }
2050
+
2051
+
2052
+ /**
2053
+ * Auto-update a plugin file
2054
+ * Returns TRUE if success
2055
+ * Returns FALSE if fallback to manual needed
2056
+ */
2057
+
2058
+
2059
+ let _serverVersions = null;
2060
+ async function fetchVersions() {
2061
+
2062
+ if (_serverVersions) return _serverVersions;
2063
+
2064
+ return new Promise((resolve) => {
2065
+
2066
+ https.get(
2067
+ "https://htmlcodeplay.com/code-play-plugin/versions.json",
2068
+ { timeout: 5000 },
2069
+ res => {
2070
+
2071
+ let data = "";
2072
+
2073
+ res.on("data", chunk => data += chunk);
2074
+
2075
+ res.on("end", () => {
2076
+
2077
+ try {
2078
+
2079
+ _serverVersions = JSON.parse(data);
2080
+
2081
+ resolve(_serverVersions);
2082
+
2083
+ } catch {
2084
+
2085
+ resolve(null);
2086
+
2087
+ }
2088
+
2089
+ });
2090
+
2091
+ }
2092
+
2093
+ ).on("error", () => resolve(null));
2094
+
2095
+ });
2096
+
2097
+ }
2098
+
2099
+ function getAgentInstructionsZipLayout(zip) {
2100
+ const entries = zip.getEntries().filter(entry => !entry.isDirectory);
2101
+
2102
+ for (const entry of entries) {
2103
+ const normalizedName = entry.entryName.replace(/\\/g, "/");
2104
+ if (
2105
+ normalizedName.startsWith("/") ||
2106
+ normalizedName.split("/").includes("..") ||
2107
+ path.isAbsolute(entry.entryName)
2108
+ ) {
2109
+ throw new Error(`Unsafe path in agent instructions ZIP: ${entry.entryName}`);
2110
+ }
2111
+ }
2112
+
2113
+ const normalizedEntries = entries.map(entry => ({
2114
+ entry,
2115
+ name: entry.entryName.replace(/\\/g, "/")
2116
+ }));
2117
+ const agentFile = normalizedEntries.find(({ name }) =>
2118
+ name.toLowerCase() === AGENT_INSTRUCTIONS_FILE.toLowerCase()
2119
+ );
2120
+ const supportFiles = normalizedEntries
2121
+ .filter(({ name }) => name.toLowerCase() !== AGENT_INSTRUCTIONS_FILE.toLowerCase())
2122
+ .map(({ entry, name }) => {
2123
+ const agentDirectoryPrefix = `${AGENT_INSTRUCTIONS_DIR.toLowerCase()}/`;
2124
+ const relativePath = name.toLowerCase().startsWith(agentDirectoryPrefix)
2125
+ ? name.slice(name.indexOf("/") + 1)
2126
+ : name;
2127
+
2128
+ return { entry, relativePath };
2129
+ })
2130
+ .filter(({ relativePath }) =>
2131
+ relativePath &&
2132
+ path.basename(relativePath).toLowerCase() !== AGENT_INSTRUCTIONS_VERSION_FILE.toLowerCase()
2133
+ );
2134
+
2135
+ if (!agentFile || !supportFiles.length) {
2136
+ throw new Error(
2137
+ `Agent instructions ZIP must contain ${AGENT_INSTRUCTIONS_FILE} (case-insensitive) and at least one supporting file.`
2138
+ );
2139
+ }
2140
+
2141
+ return { agentFile: agentFile.entry, supportFiles };
2142
+ }
2143
+
2144
+ function readInstalledAgentInstructionsVersion(projectRoot) {
2145
+ const versionFile = path.join(projectRoot, AGENT_INSTRUCTIONS_DIR, AGENT_INSTRUCTIONS_VERSION_FILE);
2146
+ if (!fs.existsSync(versionFile)) return "0";
2147
+
2148
+ const installedVersion = String(fs.readFileSync(versionFile, "utf8")).trim();
2149
+ return /^\d+(?:\.\d+)*$/.test(installedVersion) ? installedVersion : "0";
2150
+ }
2151
+
2152
+ async function syncAgentInstructions() {
2153
+ const projectRoot = process.cwd();
2154
+ const targetAgentFile = path.join(projectRoot, AGENT_INSTRUCTIONS_FILE);
2155
+ const targetAgentDir = path.join(projectRoot, AGENT_INSTRUCTIONS_DIR);
2156
+ const localVersion = readInstalledAgentInstructionsVersion(projectRoot);
2157
+
2158
+ const versions = await fetchVersions();
2159
+ const remoteVersion = String(versions?.[AGENT_INSTRUCTIONS_VERSION_KEY] || "").trim();
2160
+
2161
+ if (!/^\d+(?:\.\d+)*$/.test(remoteVersion)) {
2162
+ throw new Error(`Required agent instructions could not be resolved from the server.`);
2163
+ }
2164
+
2165
+ const remoteIsNewer = compareVersionStrings(remoteVersion, localVersion) > 0;
2166
+ if (!remoteIsNewer) {
2167
+ console.log(`✅ Agent instructions do not need an update (local ${localVersion}, online ${remoteVersion}).`);
2168
+ return;
2169
+ }
2170
+
2171
+ const zipName = `${AGENT_INSTRUCTIONS_ZIP_PREFIX}-${remoteVersion}.zip`;
2172
+ const url = `https://htmlcodeplay.com/code-play-plugin/${zipName}`;
2173
+ const tempRoot = fs.mkdtempSync(path.join(require("os").tmpdir(), "codeplay-agents-"));
2174
+ const zipPath = path.join(tempRoot, zipName);
2175
+ const extractPath = path.join(tempRoot, "extracted");
2176
+
2177
+ try {
2178
+ console.log(`🔍 Checking latest agent instructions: ${zipName}`);
2179
+ if (!(await urlExists(url))) {
2180
+ throw new Error(`Required agent instructions archive not found: ${url}`);
2181
+ }
2182
+
2183
+ await downloadFile(url, zipPath);
2184
+ console.log(`⬇️ Downloaded agent instructions archive: ${zipName}`);
2185
+ const zip = new AdmZip(zipPath);
2186
+ const archiveLayout = getAgentInstructionsZipLayout(zip);
2187
+
2188
+ const stagedAgentFile = path.join(extractPath, AGENT_INSTRUCTIONS_FILE);
2189
+ const stagedAgentDir = path.join(extractPath, AGENT_INSTRUCTIONS_DIR);
2190
+ fs.mkdirSync(stagedAgentDir, { recursive: true });
2191
+ fs.writeFileSync(stagedAgentFile, archiveLayout.agentFile.getData());
2192
+ archiveLayout.supportFiles.forEach(({ entry, relativePath }) => {
2193
+ const stagedSupportFile = path.join(stagedAgentDir, relativePath);
2194
+ fs.mkdirSync(path.dirname(stagedSupportFile), { recursive: true });
2195
+ fs.writeFileSync(stagedSupportFile, entry.getData());
2196
+ });
2197
+
2198
+ fs.rmSync(targetAgentDir, { recursive: true, force: true });
2199
+ fs.mkdirSync(targetAgentDir, { recursive: true });
2200
+ fs.copyFileSync(stagedAgentFile, targetAgentFile);
2201
+ fs.cpSync(stagedAgentDir, targetAgentDir, { recursive: true, force: true });
2202
+ fs.writeFileSync(
2203
+ path.join(targetAgentDir, AGENT_INSTRUCTIONS_VERSION_FILE),
2204
+ `${remoteVersion}\n`,
2205
+ "utf8"
2206
+ );
2207
+
2208
+ writeUpdateLine(`Agent instructions ${localVersion} -> ${remoteVersion}`);
2209
+ console.log(`✅ Agent instructions replaced in the project root → v${remoteVersion}.`);
2210
+ } catch (error) {
2211
+ throw new Error(`Required agent instructions installation failed: ${error?.message || error}`);
2212
+ } finally {
2213
+ fs.rmSync(tempRoot, { recursive: true, force: true });
2214
+ }
2215
+ }
2216
+
2217
+
2218
+
2219
+
2220
+
2221
+ async function autoUpdatePlugin(pluginDef, pluginInfo) {
2222
+
2223
+ const versions = await fetchVersions();
2224
+
2225
+ if (!versions) {
2226
+ console.log("⚠️ versions.json not reachable");
2227
+ return false;
2228
+ }
2229
+
2230
+ const oldFullPath = path.join(srcDir, pluginInfo.name);
2231
+ const oldFileName = path.basename(oldFullPath);
2232
+
2233
+ //const oldFileName = path.basename(oldFullPath);
2234
+ const oldVersionFile = oldFileName;
2235
+
2236
+
2237
+ const ext = path.extname(oldFileName); // .js or .less
2238
+ //const baseName = oldFileName.replace(/-\d+\.\d+.*$/, "");
2239
+ const baseName = oldFileName.replace(/-\d+\.\d+.*$/, '').replace(/\.(js|less)$/, '');
2240
+
2241
+ // version lookup key
2242
+ let pluginKey = baseName;
2243
+
2244
+
2245
+ // Only common plugin has js and less variants
2246
+ if (baseName === "common") {
2247
+ if (ext === ".js") pluginKey = "common-js";
2248
+ if (ext === ".less") pluginKey = "common-less";
2249
+ }
2250
+
2251
+ const latestVersion = versions[pluginKey];
2252
+
2253
+ if (!latestVersion) {
2254
+ console.log(`❌ No version entry for ${baseName}`);
2255
+ return false;
2256
+ }
2257
+
2258
+ // ===============================
2259
+ // FOLDER PLUGIN UPDATE
2260
+ // ===============================
2261
+ if (pluginDef.isFolder) {
2262
+
2263
+ const zipName = `${baseName}-${latestVersion}.zip`;
2264
+ const url = `https://htmlcodeplay.com/code-play-plugin/${zipName}`;
2265
+
2266
+ const destRoot = path.join(srcDir, pluginDef.destDir || pluginDef.baseDir || '');
2267
+ const oldPath = path.join(destRoot, pluginInfo.name);
2268
+ const newPath = path.join(destRoot, `${baseName}-${latestVersion}`);
2269
+
2270
+ if (!(await urlExists(url))) return false;
2271
+
2272
+ fs.rmSync(oldPath, { recursive: true, force: true });
2273
+
2274
+ await downloadAndExtractZip(url, newPath);
2275
+
2276
+ updateImports(pluginInfo.name, `${baseName}-${latestVersion}`);
2277
+
2278
+ // ✅ ADD THIS
2279
+ writeUpdateLine(`${pluginInfo.name} -> ${baseName}-${latestVersion}`);
2280
+
2281
+ console.log(`✅ Folder updated → ${baseName}-${latestVersion}`);
2282
+
2283
+ return true;
2284
+ }
2285
+ // ===============================
2286
+ // FILE PLUGIN UPDATE
2287
+ // ===============================
2288
+
2289
+ const pluginDir = path.dirname(oldFullPath);
2290
+
2291
+ // Only this plugin has ios variant
2292
+ const IOS_VARIANT_PLUGINS = [
2293
+ "saveToGalleryAndSaveAnyFile"
2294
+ ];
2295
+
2296
+ /* let variants = [
2297
+ `${baseName}-${latestVersion}.js`
2298
+ ]; */
2299
+
2300
+ //const ext = path.extname(oldFileName);
2301
+ const variants = [`${baseName.replace(ext,'')}-${latestVersion}${ext}`];
2302
+
2303
+
2304
+ if (IOS_VARIANT_PLUGINS.includes(baseName)) {
2305
+ variants.push(`${baseName}-${latestVersion}-ios.js`);
2306
+ }
2307
+
2308
+ let downloaded = [];
2309
+
2310
+ // Download files
2311
+ for (const fileName of variants) {
2312
+
2313
+ const url = `https://htmlcodeplay.com/code-play-plugin/${fileName}`;
2314
+
2315
+ console.log(`🔍 Checking latest: ${fileName}`);
2316
+
2317
+ if (await urlExists(url)) {
2318
+
2319
+ const destPath = path.join(pluginDir, fileName);
2320
+
2321
+ await downloadFile(url, destPath);
2322
+
2323
+ downloaded.push(fileName);
2324
+
2325
+ console.log(`⬇ Downloaded → ${fileName}`);
2326
+
2327
+ //writeUpdateLine(`Downloaded: ${fileName}`);
2328
+
2329
+ }
2330
+ }
2331
+
2332
+ if (downloaded.length === 0) {
2333
+ console.log(`❌ No files downloaded for ${baseName}`);
2334
+ return false;
2335
+ }
2336
+
2337
+ // Remove ONLY versioned files (safe)
2338
+ //const versionPattern = new RegExp(`^${baseName}-\\d+\\.\\d+(-ios)?\\.js$`);
2339
+ const versionPattern = new RegExp(`^${baseName}-\\d+\\.\\d+(-ios)?\\${ext}$`);
2340
+
2341
+ const existingFiles = fs.readdirSync(pluginDir);
2342
+
2343
+ existingFiles.forEach(file => {
2344
+
2345
+ if (
2346
+ versionPattern.test(file) &&
2347
+ !downloaded.includes(file)
2348
+ ) {
2349
+
2350
+ const oldPath = path.join(pluginDir, file);
2351
+
2352
+ fs.unlinkSync(oldPath);
2353
+
2354
+ console.log(`🗑 Removed old file → ${file}`);
2355
+ //writeUpdateLine(`Removed old file: ${file}`);
2356
+ }
2357
+
2358
+ });
2359
+
2360
+ //const newFileName = `${baseName}-${latestVersion}.js`;
2361
+ const newFileName = `${baseName}-${latestVersion}${ext}`;
2362
+
2363
+ writeUpdateLine(`${oldVersionFile} -> ${newFileName}`);
2364
+
2365
+ updateImports(oldFileName, newFileName);
2366
+ //updateImports(baseName, `${baseName}-${latestVersion}.js`);
2367
+ //updateImports(pluginInfo.name, `${baseName}-${latestVersion}`);
2368
+
2369
+ //console.log(`✅ Updated → ${newFileName}`);
2370
+ //console.log(`✅ Updated → ${baseName}-${latestVersion}`);
2371
+
2372
+ return true;
2373
+ }
2374
+
2375
+
2376
+
2377
+
2378
+ /*############################################## AUTO DOWNLOAD FROM SERVER END #####################################*/
2379
+
2380
+
2381
+
2382
+
2383
+
2384
+
2385
+
2386
+
2387
+
2388
+
2389
+
2390
+
2391
+
2392
+
2393
+
2394
+
2395
+
2396
+
2397
+ async function loadPluginVersions() {
2398
+
2399
+ if (!USE_LIVE_SERVER_VERSION) {
2400
+ console.log("ℹ️ Using local plugin versions (offline mode).");
2401
+ return;
2402
+ }
2403
+
2404
+ console.log("🌐 Fetching plugin versions from server...");
2405
+
2406
+ const versions = await fetchVersions();
2407
+
2408
+ if (!versions || typeof versions !== "object") {
2409
+ console.log("⚠️ Server unavailable or invalid versions.json. Falling back to local versions.");
2410
+ return;
2411
+ }
2412
+
2413
+ requiredPlugins.forEach(plugin => {
2414
+
2415
+ if (!plugin.name) return;
2416
+
2417
+ if (versions[plugin.name]) {
2418
+ plugin.minVersion = versions[plugin.name];
2419
+ }
2420
+
2421
+ });
2422
+
2423
+ console.log("✅ Plugin versions loaded from server.");
2424
+
2425
+ }
2426
+
2427
+
2428
+
2429
+ let hasMandatoryUpdate = false;
2430
+ function checkPlugins() {
2431
+ return new Promise(async (resolve, reject) => {
2432
+ const files = getSourceFiles();
2433
+ const outdatedPlugins = [];
2434
+ let hasMandatoryUpdate = false;
2435
+
2436
+ for (const plugin of requiredPlugins) {
2437
+ const searchRoot = getSearchRoot(plugin);
2438
+
2439
+ // ---------- Folder plugins ----------
2440
+ if (plugin.isFolder) {
2441
+ if (!fs.existsSync(searchRoot)) continue;
2442
+
2443
+ const subDirs = fs.readdirSync(searchRoot)
2444
+ .map(name => path.join(searchRoot, name))
2445
+ .filter(p => fs.statSync(p).isDirectory());
2446
+
2447
+ for (const dir of subDirs) {
2448
+ const relativePath = path.relative(searchRoot, dir).replace(/\\/g, '/');
2449
+ const match = plugin.pattern.exec(relativePath);
2450
+
2451
+ if (match) {
2452
+ const currentVersion = match[1];
2453
+
2454
+ if (compareVersions(currentVersion, plugin.minVersion) < 0) {
2455
+ outdatedPlugins.push({
2456
+ name: relativePath,
2457
+ currentVersion,
2458
+ requiredVersion: plugin.minVersion,
2459
+ mandatoryUpdate: plugin.mandatoryUpdate === true
2460
+ });
2461
+
2462
+ if (plugin.mandatoryUpdate) {
2463
+ hasMandatoryUpdate = true;
2464
+ }
2465
+ }
2466
+ }
2467
+ }
2468
+ continue;
2469
+ }
2470
+
2471
+ // ---------- File plugins ----------
2472
+ const matchedFile = files.find(file =>
2473
+ file.startsWith(searchRoot) && plugin.pattern.test(file)
2474
+ );
2475
+
2476
+ if (matchedFile) {
2477
+ const match = plugin.pattern.exec(matchedFile);
2478
+ if (match) {
2479
+ const currentVersion = match[1];
2480
+ const isBeta = !!match[2];
2481
+
2482
+ const cmp = plugin.pattern.source.includes('beta')
2483
+ ? compareWithBeta(currentVersion, plugin.minVersion, isBeta)
2484
+ : compareVersions(currentVersion, plugin.minVersion);
2485
+
2486
+ if (cmp < 0) {
2487
+ outdatedPlugins.push({
2488
+ name: path.relative(srcDir, matchedFile),
2489
+ currentVersion: isBeta ? `${currentVersion}-beta` : currentVersion,
2490
+ requiredVersion: plugin.minVersion,
2491
+ mandatoryUpdate: plugin.mandatoryUpdate === true
2492
+ });
2493
+
2494
+ if (plugin.mandatoryUpdate) {
2495
+ hasMandatoryUpdate = true;
2496
+ }
2497
+ }
2498
+ }
2499
+ }
2500
+ }
2501
+
2502
+ // ---------- Result handling ----------
2503
+ if (outdatedPlugins.length > 0) {
2504
+ console.log('\n❗ The following plugins are outdated:\n');
2505
+
2506
+ outdatedPlugins.forEach( p => {
2507
+ const tag = p.mandatoryUpdate ? '🔥 MANDATORY' : '';
2508
+ console.log(
2509
+ ` ⚠️ - ${p.name} (Current: ${p.currentVersion}, Required: ${p.requiredVersion}) ${tag}`
2510
+ );
2511
+ });
2512
+
2513
+ // 🚨 Mandatory update → stop build
2514
+ /* if (hasMandatoryUpdate) {
2515
+ console.log('\n🚫 One or more plugins require a mandatory update.');
2516
+ console.log('❌ Build cancelled. Please update mandatory plugins and try again.');
2517
+ process.exit(1);
2518
+ } */
2519
+
2520
+
2521
+
2522
+
2523
+
2524
+ if (hasMandatoryUpdate) {
2525
+
2526
+ //--------------------------------------------------
2527
+ // 🚫 AUTO UPDATE DISABLED
2528
+ //--------------------------------------------------
2529
+ if (!ENABLE_AUTO_UPDATE) {
2530
+ console.log("\n🚫 Auto-update disabled.");
2531
+ console.log("❌ Manual update required.");
2532
+ process.exit(1);
2533
+ }
2534
+
2535
+ //--------------------------------------------------
2536
+ // 🔥 AUTO UPDATE ENABLED
2537
+ //--------------------------------------------------
2538
+ console.log("\n🔥 Mandatory plugins outdated. Trying auto-update...\n");
2539
+
2540
+
2541
+
2542
+ let autoFailed = false;
2543
+
2544
+ for (const p of outdatedPlugins.filter(x => x.mandatoryUpdate)) {
2545
+
2546
+ const pluginDef = requiredPlugins.find(def =>
2547
+ def.pattern.test(p.name)
2548
+ );
2549
+
2550
+ if (!pluginDef) continue;
2551
+
2552
+ const success = await autoUpdatePlugin(
2553
+ pluginDef,
2554
+ p
2555
+ );
2556
+
2557
+ if (!success) {
2558
+ autoFailed = true;
2559
+
2560
+ const pluginDef = requiredPlugins.find(def =>
2561
+ def.pattern.test(p.name)
2562
+ );
2563
+
2564
+ console.log(`❌ Manual update required for ${p.name}`);
2565
+
2566
+ if (pluginDef) {
2567
+ console.log(`👉 Required minimum version: ${pluginDef.minVersion}`);
2568
+ }
2569
+ }
2570
+ }
2571
+
2572
+ // 🚨 Fallback to manual if any failed
2573
+ if (autoFailed) {
2574
+ console.log('\n🚫 One or more plugins require manual update.');
2575
+ console.log('❌ Build cancelled. Please update mandatory plugins.');
2576
+ process.exit(1);
2577
+ }
2578
+
2579
+ console.log('\n🎉 All mandatory plugins auto-updated! Skipping recheck for speed.\n');
2580
+
2581
+ saveUpdateLogs();
2582
+ resolve();
2583
+ return;
2584
+ }
2585
+
2586
+
2587
+
2588
+
2589
+
2590
+
2591
+ // Optional updates → ask user
2592
+ const rl = readline.createInterface({
2593
+ input: process.stdin,
2594
+ output: process.stdout
2595
+ });
2596
+
2597
+ rl.question(
2598
+ '\nAre you sure you want to continue without updating these plugins? (y/n): ',
2599
+ answer => {
2600
+ rl.close();
2601
+
2602
+ if (answer.toLowerCase() !== 'y') {
2603
+ console.log('\n❌ Build cancelled due to outdated plugins.');
2604
+ process.exit(1);
2605
+ } else {
2606
+ console.log('\n✅ Continuing build...');
2607
+ resolve();
2608
+ }
2609
+ }
2610
+ );
2611
+ } else {
2612
+ console.log('✅ All plugin versions are up to date.');
2613
+ saveUpdateLogs();
2614
+ resolve();
2615
+ }
2616
+ });
2617
+ }
2618
+
2619
+
2620
+
2621
+
2622
+
2623
+
2624
+
2625
+
2626
+
2627
+
2628
+ const localizationBaseDir = path.join(__dirname, "..", "src", "js", "localization");
2629
+
2630
+ // ======================================================
2631
+ // 🌐 LOCALIZATION CHECK (FULLY DYNAMIC)
2632
+ // ======================================================
2633
+
2634
+
2635
+ const jsRootDir = path.join(__dirname, "..", "src", "js");
2636
+
2637
+ // 🔍 Detect OLD localization files in root js/
2638
+ const oldLocalizationFiles = fs.readdirSync(jsRootDir)
2639
+ .filter(name =>
2640
+ /^localization-\d+(\.\d+)*\.js$/.test(name) ||
2641
+ /^localization_settings-\d+(\.\d+)*\.js$/.test(name)
2642
+ );
2643
+
2644
+ // ❌ If old structure found → STOP
2645
+ if (oldLocalizationFiles.length > 0) {
2646
+
2647
+ console.error(`
2648
+ ❌ OLD LOCALIZATION STRUCTURE DETECTED
2649
+
2650
+ You are using outdated file structure:
2651
+ src/js/localization-x.x.js
2652
+ src/js/localization_settings-x.x.js
2653
+
2654
+ 🚨 This is no longer supported.
2655
+
2656
+ 📦 Found files:
2657
+ ${oldLocalizationFiles.map(f => " - " + f).join("\n")}
2658
+
2659
+ 👉 Please move them to new structure:
2660
+
2661
+ src/js/localization/localization_settings-x.x.js
2662
+ src/js/localization/localization-x.x/
2663
+
2664
+ ⚠️ Example:
2665
+
2666
+ OLD:
2667
+ src/js/localization-x.x.js
2668
+ src/js/localization_settings-x.x.js
2669
+
2670
+ NEW:
2671
+ src/js/localization/localization_settings-1.1.js
2672
+ src/js/localization/localization-x.x/index.js
2673
+
2674
+ in vite.config.json
2675
+ '@translate': path.resolve(__dirname, './src/js/localization/localization-x.x'),
2676
+
2677
+ ❌ Build stopped.
2678
+ `);
2679
+
2680
+ process.exit(1);
2681
+ }
2682
+
2683
+
2684
+
2685
+
2686
+ if (fs.existsSync(localizationBaseDir)) {
2687
+
2688
+ // ❌ Block wrong files
2689
+ const invalidFiles = fs.readdirSync(localizationBaseDir)
2690
+ .filter(name => /^localization-\d+(\.\d+)*\.js$/.test(name));
2691
+
2692
+ if (invalidFiles.length > 0) {
2693
+ console.error(`
2694
+ ❌ INVALID LOCALIZATION FILE LOCATION
2695
+
2696
+ 🚫 localization-x.x.js must NOT be directly inside:
2697
+ src/js/localization/
2698
+
2699
+ 📦 Found:
2700
+ ${invalidFiles.map(f => " - " + f).join("\n")}
2701
+
2702
+ ❌ Build stopped.
2703
+ `);
2704
+ process.exit(1);
2705
+ }
2706
+
2707
+ // 🔍 Find localization-x.x folders
2708
+ const localizationVersions = fs.readdirSync(localizationBaseDir)
2709
+ .filter(name => /^localization-\d+(\.\d+)+$/.test(name));
2710
+
2711
+ if (localizationVersions.length === 0) {
2712
+ console.log("ℹ️ No localization-x.x folder found. Skipping...");
2713
+ } else {
2714
+
2715
+ // ✅ Get latest version (same logic as editor)
2716
+ const latestLocalizationDir = localizationVersions.sort((a, b) => {
2717
+ const vA = parseFloat(a.split('-')[1]);
2718
+ const vB = parseFloat(b.split('-')[1]);
2719
+ return vB - vA;
2720
+ })[0];
2721
+
2722
+ const localizationPath = path.join(localizationBaseDir, latestLocalizationDir);
2723
+
2724
+ // ======================================================
2725
+ // ✅ CHECK localization-x.x.js exists
2726
+ // ======================================================
2727
+
2728
+ const localizationFilePath = path.join(localizationPath, "index.js");
2729
+
2730
+ if (!fs.existsSync(localizationFilePath)) {
2731
+ console.error(`
2732
+ ❌ localization entry file missing
2733
+
2734
+ Expected:
2735
+ ${localizationFilePath}
2736
+
2737
+ 👉 Each localization version folder must contain:
2738
+ index.js
2739
+
2740
+ ⚠️ Example:
2741
+
2742
+ src/js/localization/localization-x.x/index.js
2743
+
2744
+ ❌ Build stopped.
2745
+ `);
2746
+ process.exit(1);
2747
+ }
2748
+
2749
+ // ======================================================
2750
+ // ✅ CHECK run.js exists
2751
+ // ======================================================
2752
+
2753
+ const runJsPath = path.join(localizationPath, "run.js");
2754
+
2755
+ if (!fs.existsSync(runJsPath)) {
2756
+ console.error(`❌ run.js not found in ${latestLocalizationDir}`);
2757
+ process.exit(1);
2758
+ }
2759
+
2760
+ // ======================================================
2761
+ // 🚀 EXECUTE run.js
2762
+ // ======================================================
2763
+
2764
+ console.log(`🌐 Localization detected: ${latestLocalizationDir}`);
2765
+ console.log(`🚀 Executing ${runJsPath}...`);
2766
+
2767
+ execSync(`node "${runJsPath}"`, { stdio: "inherit" });
2768
+ sourceFileInventory = null;
2769
+ }
2770
+
2771
+ } else {
2772
+ console.log("ℹ️ Localization not used in this project. Skipping...");
2773
+ }
2774
+
2775
+
2776
+
2777
+
2778
+
2779
+
2780
+
2781
+
2782
+
2783
+
2784
+
2785
+
2786
+
2787
+
2788
+
2789
+
2790
+
2791
+
2792
+
2793
+ const checkAndupdateDropInViteConfig = () => {
2794
+
2795
+ const possibleFiles = [
2796
+ "vite.config.js",
2797
+ "vite.config.mjs"
2798
+ ];
2799
+
2800
+ // Detect existing config file
2801
+ const viteConfigPath = possibleFiles
2802
+ .map(file => path.join(process.cwd(), file))
2803
+ .find(filePath => fs.existsSync(filePath));
2804
+
2805
+ if (!viteConfigPath) {
2806
+ console.warn("⚠️ No vite config found. Skipping.");
2807
+ return;
2808
+ }
2809
+
2810
+ //console.log("📄 Using:", viteConfigPath.split("/").pop());
2811
+
2812
+ let viteContent = fs.readFileSync(viteConfigPath, "utf8");
2813
+
2814
+ // Skip if already exists
2815
+ if (/drop\s*:\s*\[.*['"]console['"].*\]/.test(viteContent)) {
2816
+ console.log("ℹ️ vite.config.(m)js already Updated. Skipping...");
2817
+ return;
2818
+ }
2819
+
2820
+ console.log("🔧 Adding esbuild.drop ...");
2821
+
2822
+ // If esbuild block exists
2823
+ if (/esbuild\s*:\s*{/.test(viteContent)) {
2824
+ viteContent = viteContent.replace(
2825
+ /esbuild\s*:\s*{([\s\S]*?)(^ {0,8})}/m,
2826
+ (full, inner, indent) => {
2827
+
2828
+ let lines = inner
2829
+ .split("\n")
2830
+ .map(l => l.trim())
2831
+ .filter(Boolean);
2832
+
2833
+ // Fix last comma
2834
+ if (lines.length > 0) {
2835
+ lines[lines.length - 1] =
2836
+ lines[lines.length - 1].replace(/,+$/, "") + ",";
2837
+ }
2838
+
2839
+ // Re-indent
2840
+ lines = lines.map(l => indent + " " + l);
2841
+
2842
+ // Add drop
2843
+ lines.push(`${indent} drop: ['console','debugger'],`);
2844
+
2845
+ return `esbuild: {\n${lines.join("\n")}\n${indent}}`;
2846
+ }
2847
+ );
2848
+ }
2849
+
2850
+ // If esbuild missing
2851
+ else {
2852
+ viteContent = viteContent.replace(
2853
+ /export default defineConfig\s*\(\s*{/,
2854
+ m => `${m}\n esbuild: {\n drop: ['console','debugger'],\n },`
2855
+ );
2856
+ }
2857
+
2858
+ fs.writeFileSync(viteConfigPath, viteContent, "utf8");
2859
+ console.log("✅ vite.config.(m)js Updated successfully.");
2860
+ };
2861
+
2862
+ const syncAdmobAliasInViteConfig = () => {
2863
+ const latest = findAdmobPackage(path.join(process.cwd(), 'src', 'js', 'Ads'));
2864
+ if (latest) syncAdmobImports(process.cwd(), latest);
2865
+ };
2866
+
2867
+
2868
+ function getViteConfigPath() {
2869
+ const possibleFiles = [
2870
+ "vite.config.mjs",
2871
+ "vite.config.js"
2872
+ ];
2873
+
2874
+ return possibleFiles
2875
+ .map(file => path.join(process.cwd(), file))
2876
+ .find(filePath => fs.existsSync(filePath));
2877
+ };
2878
+
2879
+ const getLatestIapDir = () => {
2880
+ const adsDir = path.join(process.cwd(), "src", "js", "Ads");
2881
+
2882
+ if (!fs.existsSync(adsDir)) return null;
2883
+
2884
+ const iapDirs = fs.readdirSync(adsDir, { withFileTypes: true })
2885
+ .filter(entry => entry.isDirectory() && /^IAP-\d+(\.\d+)*$/.test(entry.name))
2886
+ .map(entry => entry.name)
2887
+ .sort((a, b) => {
2888
+ const versionA = a.match(/(\d+(?:\.\d+)*)/)[1].split(".").map(Number);
2889
+ const versionB = b.match(/(\d+(?:\.\d+)*)/)[1].split(".").map(Number);
2890
+
2891
+ for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) {
2892
+ const diff = (versionB[i] || 0) - (versionA[i] || 0);
2893
+ if (diff !== 0) return diff;
2894
+ }
2895
+
2896
+ return 0;
2897
+ });
2898
+
2899
+ return iapDirs.length > 0 ? path.join(adsDir, iapDirs[0]) : null;
2900
+ };
2901
+
2902
+ const getAnalyticsConfig = () => {
2903
+ const config = fs.existsSync(configPath)
2904
+ ? JSON.parse(fs.readFileSync(configPath, "utf8"))
2905
+ : {};
2906
+
2907
+ const analyticsFlagKey = "ANALYTICS_ENABLED";
2908
+ const androidEnabled = config.android?.[analyticsFlagKey] ?? false;
2909
+ const iosEnabled = config.ios?.[analyticsFlagKey] ?? false;
2910
+
2911
+ return {
2912
+ androidEnabled: androidEnabled === true,
2913
+ iosEnabled: iosEnabled === true,
2914
+ enabled: androidEnabled === true || iosEnabled === true
2915
+ };
2916
+ };
2917
+
2918
+ function isPackageAvailable(packageName) {
2919
+ if (!fs.existsSync(packageJsonPath)) return false;
2920
+
2921
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
2922
+
2923
+ return Boolean(
2924
+ packageJson.dependencies?.[packageName] ||
2925
+ packageJson.devDependencies?.[packageName] ||
2926
+ packageJson.optionalDependencies?.[packageName] ||
2927
+ packageJson.peerDependencies?.[packageName] ||
2928
+ fs.existsSync(path.join(process.cwd(), "node_modules", packageName, "package.json"))
2929
+ );
2930
+ };
2931
+
2932
+ const sourceImportsPackage = (packageName, ignoredFilePath) => {
2933
+ const escapedPackage = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2934
+ const importRegex = new RegExp(`(?:from\\s*['"]${escapedPackage}(?:/[^'"]*)?['"]|import\\s*['"]${escapedPackage}(?:/[^'"]*)?['"]|import\\s*\\(\\s*['"]${escapedPackage}(?:/[^'"]*)?['"]\\s*\\)|require\\s*\\(\\s*['"]${escapedPackage}(?:/[^'"]*)?['"]\\s*\\))`);
2935
+
2936
+ return getSourceFiles().some(fullPath => {
2937
+ if (ignoredFilePath && path.resolve(fullPath) === path.resolve(ignoredFilePath)) return false;
2938
+ if (!/\.(js|mjs|cjs|ts|tsx|jsx|f7)$/.test(fullPath)) return false;
2939
+ return importRegex.test(fs.readFileSync(fullPath, 'utf8'));
2940
+ });
2941
+ };
2942
+
2943
+ function runNpmCommand(command) {
2944
+ console.log(`🚀 ${command}`);
2945
+ execSync(command, { stdio: "inherit" });
2946
+ };
2947
+
2948
+ function removeOneSignalPlugin() {
2949
+ if (!isPackageAvailable(ONESIGNAL_PLUGIN_NAME)) {
2950
+ console.log('ℹ️ OneSignal is not installed. Skipping removal.');
2951
+ return;
2952
+ }
2953
+
2954
+ console.warn(`⚠️ WARNING: ${ONESIGNAL_PLUGIN_NAME} was found. OneSignal is retired and will be uninstalled automatically.`);
2955
+ runNpmCommand(`npm uninstall ${ONESIGNAL_PLUGIN_NAME}`);
2956
+ if (isPackageAvailable(ONESIGNAL_PLUGIN_NAME)) {
2957
+ throw new Error(`❌ ${ONESIGNAL_PLUGIN_NAME} is still declared or installed after npm uninstall. Build stopped; check dependent packages.`);
2958
+ }
2959
+ sourceFileInventory = null;
2960
+ console.log(`✅ Uninstalled ${ONESIGNAL_PLUGIN_NAME}.`);
2961
+ }
2962
+
2963
+ const CODEPLAY_SYSTEM_BARS_PLUGIN = "capacitor-codeplay-system-bars";
2964
+
2965
+ function ensureMandatoryCapacitorPlugins() {
2966
+ const requiredPackages = [
2967
+ "@capacitor/device",
2968
+ "@capacitor-community/in-app-review",
2969
+ "@capawesome/capacitor-app-update"
2970
+ ];
2971
+
2972
+ for (const packageName of requiredPackages) {
2973
+ const installedPackagePath = path.join(process.cwd(), "node_modules", packageName, "package.json");
2974
+ if (isPackageAvailable(packageName) && fs.existsSync(installedPackagePath)) {
2975
+ console.log(`ℹ️ ${packageName} is already installed.`);
2976
+ continue;
2977
+ }
2978
+
2979
+ runNpmCommand(`npm install ${packageName}`);
2980
+ }
2981
+ }
2982
+
2983
+ const syncCodeplaySystemBarsPackage = () => {
2984
+ if (!fs.existsSync(packageJsonPath)) {
2985
+ console.warn(`⚠️ package.json not found. Skipping ${CODEPLAY_SYSTEM_BARS_PLUGIN} package sync.`);
2986
+ return;
2987
+ }
2988
+
2989
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
2990
+ const declaredVersion = packageJson.dependencies?.[CODEPLAY_SYSTEM_BARS_PLUGIN];
2991
+ const usesLocalReference = typeof declaredVersion === "string" && declaredVersion.startsWith("file:");
2992
+
2993
+ if (usesLocalReference) {
2994
+ console.log(`ℹ️ Replacing the local ${CODEPLAY_SYSTEM_BARS_PLUGIN} reference with the npm package.`);
2995
+ runNpmCommand(`npm install ${CODEPLAY_SYSTEM_BARS_PLUGIN}`);
2996
+ return;
2997
+ }
2998
+
2999
+ if (!isPackageAvailable(CODEPLAY_SYSTEM_BARS_PLUGIN)) {
3000
+ runNpmCommand(`npm install ${CODEPLAY_SYSTEM_BARS_PLUGIN}`);
3001
+ return;
3002
+ }
3003
+
3004
+ console.log(`ℹ️ ${CODEPLAY_SYSTEM_BARS_PLUGIN} is already installed.`);
3005
+ };
3006
+
3007
+ const ADMOB_NEXTGEN_DEFAULTS = {
3008
+ VERSION: "1.6",
3009
+ ios: { nativeid: "" },
3010
+ android: { nativeid: "" },
3011
+ config: {
3012
+ bannerStartupDelayMs: "",
3013
+ interstitialStartupDelayMs: "",
3014
+ appOpenLoadOnColdStart: "",
3015
+ appOpenStartupDelayMs: "",
3016
+ useAndroidPreloadAds: true,
3017
+ useAndroidBannerPreloadAds: false,
3018
+ preloadBufferSize: 1,
3019
+ consentDebug: false,
3020
+ isKidsApp: false,
3021
+ isTesting: false,
3022
+ consentDebug: false,
3023
+ isConsoleLogEnabled: false,
3024
+ bannerEnabled: true,
3025
+ bannerDisabledPages: [],
3026
+ interstitialEnabled: true,
3027
+ appOpenEnabled: true,
3028
+ rewardVideoEnabled: false,
3029
+ rewardInterstitialEnabled: false,
3030
+ collapsibleEnabled: "",
3031
+ isLandScape: "",
3032
+ isOverlappingEnable: "",
3033
+ overlappingHeight: "",
3034
+ bannerTypeAndroid: "",
3035
+ bannerTypeAndroidLandscape: "",
3036
+ bannerTypeiOS: "",
3037
+ bannerTypeiOSLandscape: "",
3038
+ bannerTopSpaceColor: "",
3039
+ interstitialLoadScreenTextColor: "",
3040
+ interstitialLoadScreenBackgroundColor: "",
3041
+ beforeBannerSpace: "",
3042
+ whenShow: "",
3043
+ minimumClick: "",
3044
+ interstitialTimeOut: "",
3045
+ interstitialFirstTimeOut: "",
3046
+ appOpenAdsTimeOut: "",
3047
+ maxRetryCount: "",
3048
+ retrySecondsAr: "",
3049
+ appOpenPerSession: "",
3050
+ interstitialPerSession: "",
3051
+ appOpenFirstTimeOut: ""
3052
+ },
3053
+ testid: {
3054
+ nativeid: "",
3055
+ nativeid_android: "ca-app-pub-3940256099942544/2247696110",
3056
+ nativeid_ios: "ca-app-pub-3940256099942544/3986624511"
3057
+ }
3058
+ };
3059
+
3060
+ function getAdmobJsonPath() {
3061
+ const adsDir = path.join(process.cwd(), "src", "js", "Ads");
3062
+ const possiblePaths = [
3063
+ path.join(adsDir, ADMOB_NEXTGEN_CONFIG),
3064
+ path.join(adsDir, ADMOB_OLD_CONFIG)
3065
+ ];
3066
+
3067
+ return possiblePaths.find(fileExists);
3068
+ }
3069
+
3070
+ function readJsonSafe(filePath) {
3071
+ try {
3072
+ return filePath && fileExists(filePath)
3073
+ ? JSON.parse(fs.readFileSync(filePath, "utf8"))
3074
+ : null;
3075
+ } catch {
3076
+ return null;
3077
+ }
3078
+ }
3079
+
3080
+ function isKidsAdmobApp() {
3081
+ return readJsonSafe(getAdmobJsonPath())?.config?.isKidsApp === true;
3082
+ }
3083
+
3084
+ function pickFirstValue(...values) {
3085
+ return values.find(value => typeof value === "string" && value.trim() !== "") || "";
3086
+ }
3087
+
3088
+ function getAdmobJsonAppIds(config = readJsonSafe(getAdmobJsonPath())) {
3089
+ return {
3090
+ androidAppId: pickFirstValue(
3091
+ config?.androidAppId,
3092
+ config?.android_app_id,
3093
+ config?.android?.appId,
3094
+ config?.android?.appid,
3095
+ config?.android?.applicationId
3096
+ ),
3097
+ iosAppId: pickFirstValue(
3098
+ config?.iosAppId,
3099
+ config?.ios_app_id,
3100
+ config?.ios?.appId,
3101
+ config?.ios?.appid,
3102
+ config?.ios?.applicationId
3103
+ )
3104
+ };
3105
+ }
3106
+
3107
+ const mergeObjects = (template, source) => {
3108
+ if (!template || typeof template !== "object" || Array.isArray(template)) {
3109
+ return source !== undefined ? source : template;
3110
+ }
3111
+
3112
+ const output = { ...template };
3113
+
3114
+ if (!source || typeof source !== "object" || Array.isArray(source)) {
3115
+ return output;
3116
+ }
3117
+
3118
+ Object.keys(source).forEach(key => {
3119
+ if (
3120
+ template[key] &&
3121
+ typeof template[key] === "object" &&
3122
+ !Array.isArray(template[key]) &&
3123
+ source[key] &&
3124
+ typeof source[key] === "object" &&
3125
+ !Array.isArray(source[key])
3126
+ ) {
3127
+ output[key] = mergeObjects(template[key], source[key]);
3128
+ } else if (key !== "VERSION") {
3129
+ output[key] = source[key];
3130
+ }
3131
+ });
3132
+
3133
+ return output;
3134
+ };
3135
+
3136
+ const hasObjectKeys = (value) => {
3137
+ return value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
3138
+ };
3139
+
3140
+ const ensureAdmobNextgenConfigDefaults = (config) => {
3141
+ const normalizedConfig = { ...config };
3142
+ const runtimeConfig = hasObjectKeys(config?.config) ? config.config : {};
3143
+
3144
+ normalizedConfig.config = mergeObjects(ADMOB_NEXTGEN_DEFAULTS.config, runtimeConfig);
3145
+
3146
+ return normalizedConfig;
3147
+ };
3148
+
3149
+ const mergeNextgenConfigWithoutReplacingExisting = (defaultConfig, oldConfig, existingNextgenConfig) => {
3150
+ const defaultWithOldConfig = mergeObjects(defaultConfig, oldConfig);
3151
+ return mergeObjects(defaultWithOldConfig, existingNextgenConfig);
3152
+ };
3153
+
3154
+ const buildNextgenAdmobConfigFromOldConfig = (templateConfig, existingNextgenConfig, oldConfig) => {
3155
+ const mergedConfig = mergeNextgenConfigWithoutReplacingExisting(templateConfig, oldConfig, existingNextgenConfig);
3156
+
3157
+ const templateVersion = templateConfig.VERSION || ADMOB_NEXTGEN_DEFAULTS.VERSION;
3158
+ mergedConfig.VERSION = compareVersionStrings(existingNextgenConfig?.VERSION || '0', templateVersion) > 0
3159
+ ? existingNextgenConfig.VERSION : templateVersion;
3160
+
3161
+ return ensureAdmobNextgenConfigDefaults(mergedConfig);
3162
+ };
3163
+
3164
+ const syncAdmobNextgenPackage = () => {
3165
+ if (!fs.existsSync(packageJsonPath)) {
3166
+ console.warn("⚠️ package.json not found. Skipping AdMob nextgen package sync.");
3167
+ return;
3168
+ }
3169
+
3170
+ if (isPackageAvailable("emi-indo-cordova-plugin-admob")) {
3171
+ runNpmCommand("npm uninstall emi-indo-cordova-plugin-admob");
3172
+ } else {
3173
+ console.log("ℹ️ emi-indo-cordova-plugin-admob is not installed.");
3174
+ }
3175
+
3176
+ if (!isPackageAvailable("capacitor-admob-nextgen")) {
3177
+ runNpmCommand("npm install capacitor-admob-nextgen");
3178
+ } else {
3179
+ console.log("ℹ️ capacitor-admob-nextgen is already installed.");
3180
+ }
3181
+ };
3182
+
3183
+ const downloadOptionalCodeplayFile = async (fileName, destPath) => {
3184
+ const url = `https://htmlcodeplay.com/code-play-plugin/${fileName}`;
3185
+
3186
+ if (!(await urlExists(url))) {
3187
+ return false;
3188
+ }
3189
+
3190
+ await downloadFile(url, destPath);
3191
+ console.log(`⬇ Downloaded → ${fileName}`);
3192
+ return true;
3193
+ };
3194
+
3195
+ const syncAdmobConfigImports = () => {
3196
+ const adsDir = path.join(process.cwd(), "src", "js", "Ads");
3197
+ if (!fs.existsSync(adsDir)) return;
3198
+
3199
+ // Downloads invalidate the inventory; content-only migrations reuse it.
3200
+ for (const fullPath of getSourceFiles()) {
3201
+ if (!fullPath.startsWith(adsDir + path.sep)) continue;
3202
+ if (!/\.(js|mjs|ts|f7)$/.test(fullPath) || fullPath.endsWith('.min.js')) continue;
3203
+ let content = fs.readFileSync(fullPath, 'utf8');
3204
+ if (!content.includes(ADMOB_OLD_CONFIG)) continue;
3205
+ content = content.split(ADMOB_OLD_CONFIG).join(ADMOB_NEXTGEN_CONFIG);
3206
+ fs.writeFileSync(fullPath, content, 'utf8');
3207
+ console.log(`✏️ Updated AdMob config import in ${path.relative(process.cwd(), fullPath)}`);
3208
+ }
3209
+
3210
+ };
3211
+
3212
+ const ensureAdmobNextgenFiles = async () => {
3213
+ const adsDir = path.join(process.cwd(), "src", "js", "Ads");
3214
+ if (!fs.existsSync(adsDir)) {
3215
+ console.log("ℹ️ Ads folder not found. Skipping AdMob nextgen file sync.");
3216
+ return;
3217
+ }
3218
+
3219
+ await syncAdmobNextgen({ projectRoot: process.cwd(), versions: await fetchVersions() });
3220
+ sourceFileInventory = null;
3221
+ const nextgenConfigPath = path.join(adsDir, ADMOB_NEXTGEN_CONFIG);
3222
+ const oldConfigPath = path.join(adsDir, ADMOB_OLD_CONFIG);
3223
+ const templateDir = path.join(process.cwd(), 'agent-temp', 'admob-config-sync');
3224
+ fs.mkdirSync(templateDir, { recursive: true });
3225
+ const tempTemplatePath = path.join(templateDir, ADMOB_NEXTGEN_CONFIG);
3226
+
3227
+ let templateConfig = ADMOB_NEXTGEN_DEFAULTS;
3228
+
3229
+ if (await downloadOptionalCodeplayFile(ADMOB_NEXTGEN_CONFIG, tempTemplatePath)) {
3230
+ const downloadedTemplateConfig = JSON.parse(fs.readFileSync(tempTemplatePath, "utf8"));
3231
+ templateConfig = mergeObjects(ADMOB_NEXTGEN_DEFAULTS, downloadedTemplateConfig);
3232
+ templateConfig.VERSION = downloadedTemplateConfig.VERSION || ADMOB_NEXTGEN_DEFAULTS.VERSION;
3233
+ fs.unlinkSync(tempTemplatePath);
3234
+ } else if (fs.existsSync(nextgenConfigPath)) {
3235
+ templateConfig = mergeObjects(ADMOB_NEXTGEN_DEFAULTS, JSON.parse(fs.readFileSync(nextgenConfigPath, "utf8")));
3236
+ }
3237
+
3238
+ const oldConfig = fs.existsSync(oldConfigPath)
3239
+ ? JSON.parse(fs.readFileSync(oldConfigPath, "utf8"))
3240
+ : {};
3241
+ const existingNextgenConfig = fs.existsSync(nextgenConfigPath)
3242
+ ? JSON.parse(fs.readFileSync(nextgenConfigPath, "utf8"))
3243
+ : {};
3244
+
3245
+ const mergedConfig = buildNextgenAdmobConfigFromOldConfig(templateConfig, existingNextgenConfig, oldConfig);
3246
+
3247
+ const capacitorConfig = readJsonSafe(configPath) || {};
3248
+ const legacyAdmobConfig = capacitorConfig.plugins?.AdMob || {};
3249
+ const mergedAppIds = getAdmobJsonAppIds(mergedConfig);
3250
+ const androidAppId = mergedAppIds.androidAppId || legacyAdmobConfig.APP_ID_ANDROID || "";
3251
+ const iosAppId = mergedAppIds.iosAppId || legacyAdmobConfig.APP_ID_IOS || "";
3252
+
3253
+ if (androidAppId && !mergedAppIds.androidAppId) {
3254
+ mergedConfig.androidAppId = androidAppId;
3255
+ }
3256
+
3257
+ if (iosAppId && !mergedAppIds.iosAppId) {
3258
+ mergedConfig.iosAppId = iosAppId;
3259
+ }
3260
+
3261
+ if (!fs.existsSync(nextgenConfigPath)) sourceFileInventory = null;
3262
+ fs.writeFileSync(nextgenConfigPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf8");
3263
+ syncAdmobConfigImports();
3264
+ console.log("✅ AdMob nextgen files and configuration are ready.");
3265
+ };
3266
+
3267
+ const syncAdmobNextgenMigration = async () => {
3268
+ syncAdmobNextgenPackage();
3269
+ await ensureAdmobNextgenFiles();
3270
+ syncAdmobAliasInViteConfig();
3271
+ };
3272
+
3273
+ const syncSystemBarsSafeAreaMigration = () => {
3274
+ if (isPackageAvailable("@capacitor-community/safe-area")) {
3275
+ runNpmCommand("npm uninstall @capacitor-community/safe-area");
3276
+ } else {
3277
+ console.log("ℹ️ @capacitor-community/safe-area is not installed.");
3278
+ }
3279
+
3280
+ if (!fs.existsSync(configPath)) {
3281
+ console.warn("⚠️ capacitor.config.json not found. Skipping SystemBars config sync.");
3282
+ return;
3283
+ }
3284
+
3285
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
3286
+ if (!config.plugins) config.plugins = {};
3287
+ if (!config.plugins.SystemBars) config.plugins.SystemBars = {};
3288
+
3289
+ if (config.plugins.SystemBars.insetsHandling !== "css") {
3290
+ config.plugins.SystemBars.insetsHandling = "css";
3291
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3292
+ console.log('✅ capacitor.config.json SystemBars.insetsHandling set to "css".');
3293
+ } else {
3294
+ console.log('ℹ️ capacitor.config.json SystemBars.insetsHandling already "css".');
3295
+ }
3296
+ };
3297
+
3298
+ const ensureFirebaseAnalyticsWrapper = (analyticsEnabled) => {
3299
+ const analyticsDir = getLatestIapDir();
3300
+ const legacyAnalyticsDir = path.join(process.cwd(), "src", "js", "analytics");
3301
+ const legacyAnalyticsFile = path.join(legacyAnalyticsDir, "firebase-analytics.js");
3302
+
3303
+ if (!analyticsDir) {
3304
+ console.warn("⚠️ No IAP-x.x folder found. Skipping Firebase analytics wrapper sync.");
3305
+ return null;
3306
+ }
3307
+
3308
+ const analyticsFile = path.join(analyticsDir, "firebase-analytics.js");
3309
+
3310
+ if (!fs.existsSync(analyticsDir)) {
3311
+ fs.mkdirSync(analyticsDir, { recursive: true });
3312
+ }
3313
+
3314
+ const activeWrapper = `import { Capacitor, registerPlugin } from '@capacitor/core';
3315
+
3316
+ const FirebaseAnalytics = registerPlugin('FirebaseAnalytics');
3317
+
3318
+ const isEnabledValue = (value) => value === true || value === 'true';
3319
+
3320
+ const canUseNativeAnalytics = () => {
3321
+ if (!Capacitor.isNativePlatform()) return false;
3322
+
3323
+ const platform = Capacitor.getPlatform();
3324
+
3325
+ if (platform === 'android') {
3326
+ return isEnabledValue(import.meta.env.VITE_ANDROID_ANALYTICS_ENABLED);
3327
+ }
3328
+
3329
+ if (platform === 'ios') {
3330
+ return isEnabledValue(import.meta.env.VITE_IOS_ANALYTICS_ENABLED);
3331
+ }
3332
+
3333
+ return false;
3334
+ };
3335
+
3336
+ const cleanParams = (params = {}) => {
3337
+ return Object.entries(params).reduce((result, [key, value]) => {
3338
+ if (value === undefined || value === null || value === '') return result;
3339
+ if (typeof value === 'number' && Number.isNaN(value)) return result;
3340
+
3341
+ result[key] = value;
3342
+ return result;
3343
+ }, {});
3344
+ };
3345
+
3346
+ const trackEvent = async (name, params = {}) => {
3347
+ if (!canUseNativeAnalytics()) return;
3348
+
3349
+ try {
3350
+ await FirebaseAnalytics.logEvent({
3351
+ name,
3352
+ params: cleanParams(params),
3353
+ });
3354
+ } catch (err) {
3355
+ console.warn(\`Firebase analytics event failed: \${name}\`, err);
3356
+ }
3357
+ };
3358
+
3359
+ const normalizeCurrency = (currency) => {
3360
+ if (typeof currency !== 'string') return '';
3361
+ return currency.trim().toUpperCase();
3362
+ };
3363
+
3364
+ const buildPurchaseParams = (params = {}) => {
3365
+ const value = Number(params.value || 0);
3366
+ const itemId = params.item_id || params.itemId || '';
3367
+ const itemName = params.item_name || params.itemName || itemId || params.store || 'purchase';
3368
+ const quantity = Number(params.quantity || 1);
3369
+ const transactionId = params.transaction_id || params.transactionId || \`\${params.store || 'store'}_\${itemId || itemName || 'purchase'}\`;
3370
+
3371
+ return cleanParams({
3372
+ transaction_id: transactionId,
3373
+ currency: normalizeCurrency(params.currency),
3374
+ value,
3375
+ items: [
3376
+ cleanParams({
3377
+ item_id: itemId,
3378
+ item_name: itemName,
3379
+ item_category: params.store || '',
3380
+ price: value,
3381
+ quantity,
3382
+ subscription_period: params.subscription_period || '',
3383
+ }),
3384
+ ],
3385
+ });
3386
+ };
3387
+
3388
+ const buildAdRevenueParams = (params = {}) => {
3389
+ const value = Number(params.value || 0);
3390
+
3391
+ return cleanParams({
3392
+ ad_platform: params.ad_platform || 'AdMob',
3393
+ ad_source: params.ad_source || 'AdMob',
3394
+ ad_unit_name: params.ad_unit_name || params.adUnitId || '',
3395
+ ad_format: params.ad_format || '',
3396
+ currency: normalizeCurrency(params.currency),
3397
+ value,
3398
+ });
3399
+ };
3400
+
3401
+ const trackMonetizationEvent = async (customEventName, params = {}) => {
3402
+ await trackEvent(customEventName, params);
3403
+ await trackEvent('purchase', buildPurchaseParams(params));
3404
+ };
3405
+
3406
+ export const trackAdRevenue = (params) => {
3407
+ trackEvent('ad_impression', buildAdRevenueParams(params));
3408
+ };
3409
+
3410
+ export const trackSubscriptionScreenViewed = (params) => {
3411
+ trackEvent('subscription_screen_view', params);
3412
+ };
3413
+
3414
+ export const trackSubscriptionCheckoutStarted = (params) => {
3415
+ trackEvent('subscription_checkout_start', params);
3416
+ };
3417
+
3418
+ export const trackSubscriptionPurchased = (params) => {
3419
+ trackMonetizationEvent('subscription_purchase', params);
3420
+ };
3421
+
3422
+ export const trackConsumablePurchased = (params) => {
3423
+ trackMonetizationEvent('consumable_purchase', params);
3424
+ };
3425
+ `;
3426
+
3427
+ const noopWrapper = `const trackEvent = () => {};
3428
+
3429
+ export const trackAdRevenue = trackEvent;
3430
+ export const trackSubscriptionScreenViewed = trackEvent;
3431
+ export const trackSubscriptionCheckoutStarted = trackEvent;
3432
+ export const trackSubscriptionPurchased = trackEvent;
3433
+ export const trackConsumablePurchased = trackEvent;
3434
+ `;
3435
+
3436
+ const nextContent = analyticsEnabled ? activeWrapper : noopWrapper;
3437
+ const analyticsFileExists = fs.existsSync(analyticsFile);
3438
+ const currentContent = analyticsFileExists ? fs.readFileSync(analyticsFile, "utf8") : "";
3439
+ if (!analyticsFileExists) sourceFileInventory = null;
3440
+
3441
+ if (currentContent !== nextContent) {
3442
+ fs.writeFileSync(analyticsFile, nextContent, "utf8");
3443
+ console.log(`✅ Firebase analytics wrapper ${analyticsEnabled ? "enabled" : "disabled"}: ${path.relative(process.cwd(), analyticsFile)}`);
3444
+ } else {
3445
+ console.log(`ℹ️ Firebase analytics wrapper already ${analyticsEnabled ? "enabled" : "disabled"}.`);
3446
+ }
3447
+
3448
+ if (fs.existsSync(legacyAnalyticsFile)) {
3449
+ fs.unlinkSync(legacyAnalyticsFile);
3450
+ sourceFileInventory = null;
3451
+ console.log("✅ Removed old Firebase analytics wrapper: src/js/analytics/firebase-analytics.js");
3452
+ }
3453
+
3454
+ if (fs.existsSync(legacyAnalyticsDir) && fs.readdirSync(legacyAnalyticsDir).length === 0) {
3455
+ fs.rmdirSync(legacyAnalyticsDir);
3456
+ }
3457
+
3458
+ return analyticsFile;
3459
+ };
3460
+
3461
+ const syncFirebaseAnalyticsPackages = () => {
3462
+ const analyticsConfig = getAnalyticsConfig();
3463
+
3464
+ if (analyticsConfig.enabled) {
3465
+ if (!isPackageAvailable("@capacitor-firebase/analytics") || !isPackageAvailable("firebase")) {
3466
+ runNpmCommand("npm install @capacitor-firebase/analytics firebase");
3467
+ } else {
3468
+ console.log("ℹ️ Firebase Analytics packages already installed.");
3469
+ }
3470
+
3471
+ ensureFirebaseAnalyticsWrapper(true);
3472
+ return;
3473
+ }
3474
+
3475
+ const analyticsFile = ensureFirebaseAnalyticsWrapper(false);
3476
+
3477
+ if (isPackageAvailable("@capacitor-firebase/analytics")) {
3478
+ runNpmCommand("npm uninstall @capacitor-firebase/analytics");
3479
+ } else {
3480
+ console.log("ℹ️ @capacitor-firebase/analytics is not installed.");
3481
+ }
3482
+
3483
+ if (isPackageAvailable("firebase")) {
3484
+ if (sourceImportsPackage("firebase", analyticsFile)) {
3485
+ console.log("ℹ️ Keeping firebase package because other source files import it.");
3486
+ } else {
3487
+ runNpmCommand("npm uninstall firebase");
3488
+ }
3489
+ }
3490
+ };
3491
+
3492
+ const syncFirebaseAnalyticsPodfile = () => {
3493
+ const podfilePath = path.join(process.cwd(), "ios", "App", "Podfile");
3494
+ if (!fs.existsSync(podfilePath)) return;
3495
+
3496
+ const { iosEnabled } = getAnalyticsConfig();
3497
+ const basePodLine = " pod 'CapacitorFirebaseAnalytics', :path => '../../node_modules/@capacitor-firebase/analytics'";
3498
+ const analyticsPodLine = " pod 'CapacitorFirebaseAnalytics/Analytics', :path => '../../node_modules/@capacitor-firebase/analytics'";
3499
+
3500
+ let content = fs.readFileSync(podfilePath, "utf8");
3501
+ let nextContent = content;
3502
+
3503
+ if (iosEnabled) {
3504
+ if (!nextContent.includes(basePodLine)) {
3505
+ nextContent = nextContent.replace(
3506
+ /(pod 'CapacitorCommunitySafeArea'[^\n]*\r?\n)/,
3507
+ `$1${basePodLine}\n`
3508
+ );
3509
+ }
3510
+
3511
+ if (!nextContent.includes(analyticsPodLine)) {
3512
+ nextContent = nextContent.replace(
3513
+ /(# Add your Pods here\r?\n)/,
3514
+ `$1${analyticsPodLine}\n`
3515
+ );
3516
+ }
3517
+ } else {
3518
+ nextContent = nextContent
3519
+ .split(/\r?\n/)
3520
+ .filter(line => !line.includes("CapacitorFirebaseAnalytics"))
3521
+ .join("\n");
3522
+ }
3523
+
3524
+ if (nextContent !== content) {
3525
+ fs.writeFileSync(podfilePath, nextContent, "utf8");
3526
+ console.log(`✅ iOS Firebase Analytics Podfile entries ${iosEnabled ? "enabled" : "removed"}.`);
3527
+ } else {
3528
+ console.log(`ℹ️ iOS Firebase Analytics Podfile entries already ${iosEnabled ? "enabled" : "disabled"}.`);
3529
+ }
3530
+ };
3531
+
3532
+ function ensureImportOrRequire(content, moduleName, esmLine, cjsLine) {
3533
+ const escaped = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3534
+ if (new RegExp(`from\\s*['"]${escaped}['"]|require\\s*\\(\\s*['"]${escaped}['"]\\s*\\)`).test(content)) {
3535
+ return content;
3536
+ }
3537
+
3538
+ const isCommonJs = /module\.exports|require\s*\(/.test(content);
3539
+ const line = isCommonJs ? cjsLine : esmLine;
3540
+
3541
+ return `${line}\n${content}`;
3542
+ };
3543
+
3544
+ const ensureViteDefineEntry = (viteContent, key, valueExpression) => {
3545
+ const entry = ` '${key}': JSON.stringify(${valueExpression}),`;
3546
+
3547
+ if (viteContent.includes(`'${key}'`) || viteContent.includes(`"${key}"`)) {
3548
+ return viteContent;
3549
+ }
3550
+
3551
+ if (/define\s*:\s*{/.test(viteContent)) {
3552
+ return viteContent.replace(/define\s*:\s*{\s*/, match => `${match}\n${entry}\n`);
3553
+ }
3554
+
3555
+ const defineBlock = ` define: {
3556
+ '${key}': JSON.stringify(${valueExpression}),
3557
+ },`;
3558
+
3559
+ if (/\n\s*publicDir\s*:\s*[^,\n]+,?/.test(viteContent)) {
3560
+ return viteContent.replace(
3561
+ /(\n\s*publicDir\s*:\s*[^,\n]+,?)/,
3562
+ `$1\n${defineBlock}`
3563
+ );
3564
+ }
3565
+
3566
+ return viteContent.replace(
3567
+ /(return\s+{)/,
3568
+ `$1\n${defineBlock}`
3569
+ );
3570
+ };
3571
+
3572
+ const ensurePrivacyAndAnalyticsViteConfig = () => {
3573
+ const viteConfigPath = getViteConfigPath();
3574
+
3575
+ if (!viteConfigPath) {
3576
+ console.warn("⚠️ No vite config found. Skipping privacy/analytics Vite sync.");
3577
+ return;
3578
+ }
3579
+
3580
+ let viteContent = fs.readFileSync(viteConfigPath, "utf8");
3581
+ const originalContent = viteContent;
3582
+
3583
+ viteContent = ensureImportOrRequire(
3584
+ viteContent,
3585
+ "fs",
3586
+ "import fs from 'fs';",
3587
+ "const fs = require('fs');"
3588
+ );
3589
+
3590
+ viteContent = ensureImportOrRequire(
3591
+ viteContent,
3592
+ "path",
3593
+ "import path from 'path';",
3594
+ "const path = require('path');"
3595
+ );
3596
+
3597
+ if (!/\bcapacitorConfig\b/.test(viteContent)) {
3598
+ viteContent = viteContent.replace(
3599
+ /(\r?\nexport\s+default|\r?\nmodule\.exports)/,
3600
+ `\nconst capacitorConfig = JSON.parse(fs.readFileSync(path.resolve(__dirname, './capacitor.config.json'), 'utf8'));\n$1`
3601
+ );
3602
+ }
3603
+
3604
+ if (!/\bAPP_UNIQUE_ID\b/.test(viteContent)) {
3605
+ viteContent = viteContent.replace(
3606
+ /(const capacitorConfig = JSON\.parse\(fs\.readFileSync\(path\.resolve\(__dirname,\s*['"]\.\/capacitor\.config\.json['"]\),\s*['"]utf8['"]\)\);\r?\n)/,
3607
+ `$1const APP_UNIQUE_ID = capacitorConfig.android?.APP_UNIQUE_ID ?? '';\n`
3608
+ );
3609
+ }
3610
+
3611
+ if (!/\bANDROID_ANALYTICS_ENABLED\b/.test(viteContent)) {
3612
+ viteContent = viteContent.replace(
3613
+ /(const APP_UNIQUE_ID = capacitorConfig\.android\?\.APP_UNIQUE_ID \?\? ['"]['"];\r?\n)/,
3614
+ `$1const ANDROID_ANALYTICS_ENABLED = capacitorConfig.android?.ANALYTICS_ENABLED ?? false;\n`
3615
+ );
3616
+ }
3617
+
3618
+ if (!/\bIOS_ANALYTICS_ENABLED\b/.test(viteContent)) {
3619
+ viteContent = viteContent.replace(
3620
+ /(const ANDROID_ANALYTICS_ENABLED = capacitorConfig\.android\?\.ANALYTICS_ENABLED \?\? false;\r?\n)/,
3621
+ `$1const IOS_ANALYTICS_ENABLED = capacitorConfig.ios?.ANALYTICS_ENABLED ?? false;\n`
3622
+ );
3623
+ }
3624
+
3625
+ if (!/\bIAP_DESIGN\b/.test(viteContent)) {
3626
+ viteContent = viteContent.replace(
3627
+ /(const IOS_ANALYTICS_ENABLED = capacitorConfig\.ios\?\.ANALYTICS_ENABLED \?\? false;\r?\n)/,
3628
+ `$1const IAP_DESIGN = capacitorConfig["IAP-Design"] ?? 'default';\n`
3629
+ );
3630
+ }
3631
+
3632
+ viteContent = viteContent
3633
+ .replace(
3634
+ /const ANDROID_ANALYTICS_ENABLED = capacitorConfig\.android\?\.ANALY[ST]ICS_ENABLED \?\? capacitorConfig\.android\?\.ANALYTICS_ENABLED \?\? false;/g,
3635
+ "const ANDROID_ANALYTICS_ENABLED = capacitorConfig.android?.ANALYTICS_ENABLED ?? false;"
3636
+ )
3637
+ .replace(
3638
+ /const IOS_ANALYTICS_ENABLED = capacitorConfig\.ios\?\.ANALY[ST]ICS_ENABLED \?\? capacitorConfig\.ios\?\.ANALYTICS_ENABLED \?\? false;/g,
3639
+ "const IOS_ANALYTICS_ENABLED = capacitorConfig.ios?.ANALYTICS_ENABLED ?? false;"
3640
+ );
3641
+
3642
+ viteContent = ensureViteDefineEntry(viteContent, "import.meta.env.VITE_APP_UNIQUE_ID", "APP_UNIQUE_ID");
3643
+ viteContent = ensureViteDefineEntry(viteContent, "import.meta.env.VITE_ANDROID_ANALYTICS_ENABLED", "ANDROID_ANALYTICS_ENABLED");
3644
+ viteContent = ensureViteDefineEntry(viteContent, "import.meta.env.VITE_IOS_ANALYTICS_ENABLED", "IOS_ANALYTICS_ENABLED");
3645
+ viteContent = ensureViteDefineEntry(viteContent, "import.meta.env.VITE_IAP_DESIGN", "IAP_DESIGN");
3646
+
3647
+ if (viteContent !== originalContent) {
3648
+ fs.writeFileSync(viteConfigPath, viteContent, "utf8");
3649
+ console.log("✅ vite.config.(m)js privacy/analytics settings synced.");
3650
+ } else {
3651
+ console.log("ℹ️ vite.config.(m)js privacy/analytics settings already synced.");
3652
+ }
3653
+ };
3654
+
3655
+ const syncFirebaseAnalyticsSetup = () => {
3656
+ syncFirebaseAnalyticsPackages();
3657
+ syncFirebaseAnalyticsPodfile();
3658
+ ensurePrivacyAndAnalyticsViteConfig();
3659
+ };
3660
+
3661
+ const getAdmobIapConfigPath = () => {
3662
+ const possiblePaths = [
3663
+ path.join(process.cwd(), 'src', 'js', 'Ads', 'admob-ad-configuration-nextgen.json'),
3664
+ path.join(process.cwd(), 'src', 'js', 'Ads', 'admob-ad-configuration.json')
3665
+ ];
3666
+
3667
+ return possiblePaths.find(fileExists);
3668
+ };
3669
+
3670
+ const getAdmobIapConfig = () => {
3671
+ const admobIapConfigPath = getAdmobIapConfigPath();
3672
+
3673
+ if (!admobIapConfigPath) {
3674
+ console.warn("⚠️ No AdMob configuration JSON found. Skipping RevenueCat IAP package sync.");
3675
+ return null;
3676
+ }
3677
+
3678
+ try {
3679
+ return JSON.parse(fs.readFileSync(admobIapConfigPath, "utf8"));
3680
+ } catch (err) {
3681
+ console.error(`❌ Failed to read ${path.relative(process.cwd(), admobIapConfigPath)}`, err);
3682
+ process.exit(1);
3683
+ }
3684
+ };
3685
+
3686
+ const isAnyIapStoreEnabled = (iapConfig) => {
3687
+ return Boolean(
3688
+ iapConfig?.playstore === true ||
3689
+ iapConfig?.samsung === true ||
3690
+ iapConfig?.amazon === true ||
3691
+ iapConfig?.ios === true
3692
+ );
3693
+ };
3694
+
3695
+ const ensureIapDesignInCapacitorConfig = (iapConfig) => {
3696
+ if (!isAnyIapStoreEnabled(iapConfig)) return;
3697
+
3698
+ if (!fs.existsSync(configPath)) {
3699
+ console.warn("⚠️ capacitor.config.json not found. Skipping IAP-Design sync.");
3700
+ return;
3701
+ }
3702
+
3703
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
3704
+
3705
+ if (Object.prototype.hasOwnProperty.call(config, "IAP-Design")) {
3706
+ console.log("ℹ️ capacitor.config.json IAP-Design already configured.");
3707
+ return;
3708
+ }
3709
+
3710
+ config["IAP-Design"] = "default";
3711
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
3712
+ console.log('✅ Added "IAP-Design": "default" to capacitor.config.json.');
3713
+ };
3714
+
3715
+ const getCurrentStoreId = () => {
3716
+ const envStoreId = Number(process.env.VITE_STORE_ID);
3717
+ if (Number.isInteger(envStoreId) && envStoreId > 0) return envStoreId;
3718
+
3719
+ const viteConfigPath = getViteConfigPath();
3720
+ if (!viteConfigPath) return 1;
3721
+
3722
+ const viteConfigContent = fs.readFileSync(viteConfigPath, "utf8");
3723
+ const commonAliasMatch = viteConfigContent.match(/['"]@common['"]:\s*path\.resolve\(__dirname,\s*['"]([^'"]+)['"]\)/);
3724
+
3725
+ if (!commonAliasMatch) return 1;
3726
+
3727
+ const commonFilePath = path.resolve(path.dirname(viteConfigPath), commonAliasMatch[1]);
3728
+ if (!fs.existsSync(commonFilePath)) return 1;
3729
+
3730
+ const commonFileContent = fs.readFileSync(commonFilePath, "utf8");
3731
+ const environmentStoreIdMatch = commonFileContent.match(
3732
+ /^[ \t]*export\s+let\s+_storeid\s*=\s*import\.meta\.env\.VITE_STORE_ID\s*\|\|\s*(\d+)\s*;/m
3733
+ );
3734
+ if (environmentStoreIdMatch) return Number(environmentStoreIdMatch[1]);
3735
+
3736
+ const directStoreIdMatch = commonFileContent.match(
3737
+ /^[ \t]*export\s+let\s+_storeid\s*=\s*(\d+)\s*;/m
3738
+ );
3739
+
3740
+ return directStoreIdMatch ? Number(directStoreIdMatch[1]) : 1;
3741
+ };
3742
+
3743
+ const getRevenueCatStoreState = (iapConfig) => {
3744
+ const storeId = getCurrentStoreId();
3745
+ const configuration = iapConfig.configuration || {};
3746
+ const storeMap = {
3747
+ 1: {
3748
+ name: "PlayStore",
3749
+ enabled: iapConfig.playstore === true,
3750
+ apiKeyName: "IAP.configuration.playstore",
3751
+ apiKey: configuration.playstore,
3752
+ usesGoogleBilling: true
3753
+ },
3754
+ 6: {
3755
+ name: "iOSStore",
3756
+ enabled: iapConfig.ios === true,
3757
+ apiKeyName: "IAP.configuration.ios",
3758
+ apiKey: configuration.ios,
3759
+ usesGoogleBilling: false
3760
+ },
3761
+ 7: {
3762
+ name: "AmazonStore",
3763
+ enabled: iapConfig.amazon === true,
3764
+ apiKeyName: "IAP.configuration.amazon",
3765
+ apiKey: configuration.amazon,
3766
+ usesGoogleBilling: false
3767
+ }
3768
+ };
3769
+
3770
+ return {
3771
+ storeId,
3772
+ store: storeMap[storeId] || null
3773
+ };
3774
+ };
3775
+
3776
+ const syncAmazonFireWebViewDependency = (enabled) => {
3777
+ if (!fs.existsSync(androidAppBuildGradlePath)) {
3778
+ console.error(`❌ Android app build.gradle not found: ${androidAppBuildGradlePath}`);
3779
+ process.exit(1);
3780
+ }
3781
+
3782
+ const currentContent = fs.readFileSync(androidAppBuildGradlePath, "utf8");
3783
+ let nextContent = currentContent.replace(AMAZON_WINDOW_EXTENSIONS_DEPENDENCY_PATTERN, "");
3784
+
3785
+ if (enabled) {
3786
+ const newline = nextContent.includes("\r\n") ? "\r\n" : "\n";
3787
+ const dependenciesHeaderPattern = /(^dependencies[ \t]*\{[ \t]*\r?\n)/m;
3788
+
3789
+ if (!dependenciesHeaderPattern.test(nextContent)) {
3790
+ console.error(`❌ dependencies block not found in ${androidAppBuildGradlePath}`);
3791
+ process.exit(1);
3792
+ }
3793
+
3794
+ nextContent = nextContent.replace(
3795
+ dependenciesHeaderPattern,
3796
+ `$1 ${AMAZON_WINDOW_EXTENSIONS_DEPENDENCY}${newline}`
3797
+ );
3798
+ }
3799
+
3800
+ if (nextContent !== currentContent) {
3801
+ fs.writeFileSync(androidAppBuildGradlePath, nextContent, "utf8");
3802
+ }
3803
+
3804
+ console.log(enabled
3805
+ ? "✅ Amazon Fire WebView compatibility dependency enabled for Store ID 7."
3806
+ : "ℹ️ Amazon Fire WebView compatibility dependency disabled for this store.");
3807
+ };
3808
+
3809
+ function syncAndroidAdIdPermission() {
3810
+ if (!fs.existsSync(androidManifestPath)) return;
3811
+ const originalContent = fs.readFileSync(androidManifestPath, 'utf8');
3812
+ const newline = originalContent.includes('\r\n') ? '\r\n' : '\n';
3813
+ const marker = '<!-- Codeplay: child-directed advertising ID exclusion -->';
3814
+ const permission = 'com.google.android.gms.permission.AD_ID';
3815
+ const permissionTag = `<uses-permission android:name="${permission}" tools:node="remove" />`;
3816
+ const generatedRule = new RegExp(`^[ \\t]*${marker}\\r?\\n[ \\t]*<uses-permission\\b[^>]*tools:node="remove"[^>]*\\/>\\r?\\n`, 'gm');
3817
+ const hadGeneratedRule = originalContent.includes(marker);
3818
+ let content = originalContent.replace(generatedRule, '');
3819
+ if (isKidsAdmobApp()) {
3820
+ content = content.replace(/(?:^[ \t]*)?<uses-permission\b[^>]*>(?:[ \t\r\n]*<\/uses-permission>)?[ \t]*(?:\r?\n)?/gm, tag =>
3821
+ /android:name\s*=\s*["']com\.google\.android\.gms\.permission\.AD_ID["']/.test(tag) ? '' : tag);
3822
+ if (!/xmlns:tools\s*=/.test(content)) {
3823
+ content = content.replace(/<manifest\b/, '<manifest xmlns:tools="http://schemas.android.com/tools"');
3824
+ }
3825
+ content = content.replace(/(<manifest\b[^>]*>)(?:\r?\n)?/, (match, root) => `${root}${newline} ${marker}${newline} ${permissionTag}${newline}`);
3826
+ } else if (hadGeneratedRule) {
3827
+ content = content.replace(/(<manifest\b[^>]*>)(?:\r?\n)?/, (match, root) => `${root}${newline} <uses-permission android:name="${permission}" />${newline}`);
3828
+ }
3829
+ if (content !== originalContent) {
3830
+ fs.writeFileSync(androidManifestPath, content, 'utf8');
3831
+ console.log(isKidsAdmobApp() ? 'Android advertising ID permission excluded for this kids app.' : 'Android advertising ID permission restored for this non-kids app.');
3832
+ }
3833
+ }
3834
+
3835
+ const ensureAndroidPermission = (permission, enabled) => {
3836
+ if (!fs.existsSync(androidManifestPath)) return false;
3837
+
3838
+ let manifestContent = fs.readFileSync(androidManifestPath, "utf8");
3839
+ const permissionLine = ` <uses-permission android:name="${permission}" />`;
3840
+ const permissionRegex = new RegExp(`^\\s*<uses-permission\\s+android:name="${permission.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*/?>\\s*\\r?\\n?`, "m");
3841
+
3842
+ if (enabled) {
3843
+ if (permissionRegex.test(manifestContent)) return false;
3844
+
3845
+ manifestContent = manifestContent.replace(
3846
+ /<\/manifest>\s*$/,
3847
+ `${permissionLine}\n</manifest>`
3848
+ );
3849
+ fs.writeFileSync(androidManifestPath, manifestContent, "utf8");
3850
+ console.log(`✅ Added Android permission: ${permission}`);
3851
+ return true;
3852
+ }
3853
+
3854
+ if (!permissionRegex.test(manifestContent)) return false;
3855
+
3856
+ manifestContent = manifestContent.replace(permissionRegex, "");
3857
+ fs.writeFileSync(androidManifestPath, manifestContent, "utf8");
3858
+ console.log(`✅ Removed Android permission: ${permission}`);
3859
+ return true;
3860
+ };
3861
+
3862
+ const syncRevenueCatIapSetup = () => {
3863
+ const admobConfig = getAdmobIapConfig();
3864
+ const iapConfig = admobConfig?.IAP;
3865
+
3866
+ if (!iapConfig) {
3867
+ syncAmazonFireWebViewDependency(false);
3868
+ console.log("ℹ️ IAP configuration not found. Skipping RevenueCat package sync.");
3869
+ return;
3870
+ }
3871
+
3872
+ ensureIapDesignInCapacitorConfig(iapConfig);
3873
+
3874
+ const revenueCatEnabled =
3875
+ iapConfig.playstore === true ||
3876
+ iapConfig.ios === true ||
3877
+ iapConfig.amazon === true;
3878
+ const { storeId, store } = getRevenueCatStoreState(iapConfig);
3879
+ const revenueCatEnabledForCurrentStore = store?.enabled === true;
3880
+ syncAmazonFireWebViewDependency(storeId === 7 && iapConfig.amazon === true);
3881
+
3882
+ if (revenueCatEnabledForCurrentStore && !store.apiKey) {
3883
+ console.error(`❌ RevenueCat IAP is enabled for ${store.name} but ${store.apiKeyName} is missing.`);
3884
+ process.exit(1);
3885
+ }
3886
+
3887
+ let packageChanged = false;
3888
+
3889
+ if (revenueCatEnabledForCurrentStore) {
3890
+ if (!isPackageAvailable("@revenuecat/purchases-capacitor")) {
3891
+ runNpmCommand("npm install @revenuecat/purchases-capacitor");
3892
+ packageChanged = true;
3893
+ } else {
3894
+ console.log(`ℹ️ RevenueCat package already installed for ${store.name}.`);
3895
+ }
3896
+ } else if (isPackageAvailable("@revenuecat/purchases-capacitor")) {
3897
+ runNpmCommand("npm uninstall @revenuecat/purchases-capacitor");
3898
+ packageChanged = true;
3899
+ } else {
3900
+ console.log(`ℹ️ RevenueCat package is not required for store id ${storeId}.`);
3901
+ }
3902
+
3903
+ ensureAndroidPermission("com.android.vending.BILLING", store?.usesGoogleBilling === true && revenueCatEnabledForCurrentStore);
3904
+
3905
+ if (iapConfig.samsung === true && revenueCatEnabled) {
3906
+ console.warn("⚠️ Samsung IAP uses cordova-plugin-samsungiap; PlayStore/Amazon/iOS use RevenueCat.");
3907
+ }
3908
+
3909
+ if (packageChanged) {
3910
+ runNpmCommand("npx cap sync");
3911
+ }
3912
+ };
3913
+
3914
+
3915
+
3916
+
3917
+
3918
+
3919
+
3920
+
3921
+
3922
+ const compareVersion = (v1, v2) => {
3923
+ const a = v1.split(".").map(Number);
3924
+ const b = v2.split(".").map(Number);
3925
+
3926
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
3927
+ const num1 = a[i] || 0;
3928
+ const num2 = b[i] || 0;
3929
+ if (num1 > num2) return 1;
3930
+ if (num1 < num2) return -1;
3931
+ }
3932
+ return 0;
3933
+ };
3934
+
3935
+
3936
+
3937
+
3938
+
3939
+
3940
+
3941
+
3942
+
3943
+
3944
+ const admobConfigPaths = [
3945
+ path.join('src', 'js', 'Ads', 'admob-ad-configuration-nextgen.json'),
3946
+ path.join('src', 'js', 'Ads', 'admob-ad-configuration.json')
3947
+ ];
3948
+
3949
+ const checkAdmobConfigurationProperty=()=>{
3950
+
3951
+
3952
+ if (!_admobConfig.ADMOB_ENABLED)
3953
+ {
3954
+ console.log("ℹ️ Admob is not enabled so ad configuration checking is skipping...");
3955
+ return;
3956
+ }
3957
+
3958
+ const admobConfigPath = admobConfigPaths.find(fileExists);
3959
+
3960
+ if (!admobConfigPath) {
3961
+ console.error("❌ Failed to find AdMob configuration JSON. Checked admob-ad-configuration-nextgen.json and admob-ad-configuration.json");
3962
+ process.exit(1);
3963
+ }
3964
+
3965
+
3966
+ const REQUIRED_CONFIG_KEYS = [
3967
+ "isKidsApp",
3968
+ "isTesting",
3969
+ "consentDebug",
3970
+ "isConsoleLogEnabled",
3971
+ "bannerEnabled",
3972
+ "bannerDisabledPages",
3973
+ "interstitialEnabled",
3974
+ "appOpenEnabled",
3975
+ "rewardVideoEnabled",
3976
+ "rewardInterstitialEnabled",
3977
+ "collapsibleEnabled",
3978
+ "isLandScape",
3979
+ "isOverlappingEnable",
3980
+ "overlappingHeight",
3981
+ "bannerTypeAndroid",
3982
+ "bannerTypeAndroidLandscape",
3983
+ "bannerTypeiOS",
3984
+ "bannerTypeiOSLandscape",
3985
+ "bannerTopSpaceColor",
3986
+ "interstitialLoadScreenTextColor",
3987
+ "interstitialLoadScreenBackgroundColor",
3988
+ "beforeBannerSpace",
3989
+ "whenShow",
3990
+ "minimumClick",
3991
+ "interstitialTimeOut",
3992
+ "interstitialFirstTimeOut",
3993
+ "appOpenAdsTimeOut",
3994
+ "maxRetryCount",
3995
+ "retrySecondsAr",
3996
+ "appOpenPerSession",
3997
+ "interstitialPerSession",
3998
+ "appOpenFirstTimeOut"
3999
+ ];
4000
+
4001
+
4002
+
4003
+
4004
+
4005
+ let admobConfigInJson;
4006
+
4007
+ try {
4008
+ admobConfigInJson = JSON.parse(readFileSync(admobConfigPath, "utf8"));
4009
+ } catch (err) {
4010
+ console.error(`❌ Failed to read ${admobConfigPath}`, err);
4011
+ process.exit(1);
4012
+ }
4013
+
4014
+ // ✅ Validate config object exists
4015
+ if (!admobConfigInJson.config) {
4016
+ console.error(`❌ "config" object is missing in ${admobConfigPath}`);
4017
+ process.exit(1);
4018
+ }
4019
+
4020
+
4021
+ const admobConfigMinVersion="1.0"
4022
+
4023
+ if (compareVersion(admobConfigInJson.VERSION, admobConfigMinVersion) < 0) {
4024
+ console.error(`❌ Please use at-least version ${admobConfigMinVersion} in "${admobConfigPath}"`);
4025
+ process.exit(1);
4026
+ }
4027
+
4028
+
4029
+ const config = admobConfigInJson.config;
4030
+
4031
+ // ✅ Find missing properties
4032
+ const missingKeys = REQUIRED_CONFIG_KEYS.filter(
4033
+ key => !(key in config)
4034
+ );
4035
+
4036
+
4037
+
4038
+ if (missingKeys.length > 0) {
4039
+ console.error(`❌ Missing required configuration keys. Please check it in '${admobConfigPath}'`);
4040
+
4041
+ missingKeys.forEach(k => console.error(" - " + k));
4042
+ process.exit(1);
4043
+ }
4044
+
4045
+
4046
+ console.log(`✅ All keys exist in "${admobConfigPath}". Configuration looks good.`);
4047
+ }
4048
+
4049
+
4050
+
4051
+ function ensureGitignoreEntry(entry, shouldIgnore = true) {
4052
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
4053
+
4054
+ // If .gitignore doesn't exist, create it
4055
+ if (!fs.existsSync(gitignorePath)) {
4056
+ if (!shouldIgnore) return;
4057
+ fs.writeFileSync(gitignorePath, `${entry}\n`, 'utf8');
4058
+ console.log(`✅ .gitignore created and added: ${entry}`);
4059
+ return;
4060
+ }
4061
+
4062
+ const content = fs.readFileSync(gitignorePath, 'utf8');
4063
+
4064
+ if (!shouldIgnore) {
4065
+ const normalizedEntry = entry.replace(/^\/|\/$/g, '');
4066
+ const nextContent = content.split(/(?<=\n)/).filter(line =>
4067
+ line.trim().replace(/^\/|\/$/g, '') !== normalizedEntry
4068
+ ).join('');
4069
+ if (nextContent !== content) {
4070
+ fs.writeFileSync(gitignorePath, nextContent, 'utf8');
4071
+ console.log(`✅ Removed from .gitignore: ${entry}`);
4072
+ }
4073
+ return;
4074
+ }
4075
+
4076
+ // Normalize lines (trim + remove trailing slashes for comparison)
4077
+ const lines = content
4078
+ .split(/\r?\n/)
4079
+ .map(l => l.trim());
4080
+
4081
+ const normalizedEntry = entry.replace(/\/$/, '');
4082
+
4083
+ const exists = lines.some(
4084
+ line => line.replace(/\/$/, '') === normalizedEntry
4085
+ );
4086
+
4087
+ if (exists) {
4088
+ console.log(`ℹ️ .gitignore already contains: ${entry}`);
4089
+ return;
4090
+ }
4091
+
4092
+ // Ensure file ends with newline
4093
+ const separator = content.endsWith('\n') ? '' : '\n';
4094
+
4095
+ fs.appendFileSync(gitignorePath, `${separator}${entry}\n`, 'utf8');
4096
+ console.log(`✅ Added to .gitignore: ${entry}`);
4097
+ }
4098
+
4099
+
4100
+ ensureGitignoreEntry('buildCodeplay/', false);
4101
+ ensureGitignoreEntry('agent-temp/');
4102
+
4103
+
4104
+ // ======================================================
4105
+ // Validate theme folder location (src/js/theme is NOT allowed)
4106
+ // ======================================================
4107
+
4108
+ function validateThemeFolderLocation() {
4109
+ const oldThemePath = path.join(process.cwd(), 'src', 'js', 'theme');
4110
+ const newThemePath = path.join(process.cwd(), 'src', 'theme');
4111
+
4112
+ // ❌ Block old structure
4113
+ if (fs.existsSync(oldThemePath)) {
4114
+ console.error(
4115
+ '\n❌ INVALID PROJECT STRUCTURE DETECTED\n' +
4116
+ '--------------------------------------------------\n' +
4117
+ 'The "theme" folder must NOT be inside:\n' +
4118
+ ' src/js/theme\n\n' +
4119
+ '✅ Correct location is:\n' +
4120
+ ' src/theme\n\n' +
4121
+ '🛑 Please move the folder and re-run the build.\n'
4122
+ );
4123
+ process.exit(1);
4124
+ }
4125
+
4126
+ // ⚠️ Optional warning if new theme folder is missing
4127
+ if (!fs.existsSync(newThemePath)) {
4128
+ console.warn(
4129
+ '\n⚠️ WARNING: "src/theme" folder not found.\n' +
4130
+ 'If your app uses themes, please ensure it exists.\n'
4131
+ );
4132
+ } else {
4133
+ console.log('✅ Theme folder structure validated (src/theme).');
4134
+ }
4135
+ }
4136
+ const validateAndRestoreSignDetails=()=>{
4137
+
4138
+ // Read config file
4139
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
4140
+
4141
+ // Ensure android and buildOptions exist
4142
+ if (!config.android) config.android = {};
4143
+ if (!config.android.buildOptions) config.android.buildOptions = {};
4144
+
4145
+ // Update only if changed
4146
+ let updated = false;
4147
+
4148
+ if (config.android.buildOptions.releaseType !== 'AAB') {
4149
+ config.android.buildOptions.releaseType = 'AAB';
4150
+ updated = true;
4151
+ }
4152
+
4153
+ if (config.android.buildOptions.signingType !== 'jarsigner') {
4154
+ config.android.buildOptions.signingType = 'jarsigner';
4155
+ updated = true;
4156
+ }
4157
+
4158
+ // Write back only if modified
4159
+ if (updated) {
4160
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
4161
+ console.log('capacitor.config.json updated successfully.');
4162
+ } else {
4163
+ console.log('No changes needed.');
4164
+ }
4165
+
4166
+ }
4167
+
4168
+ //################################## SystemBars.java update for "@capacitor/android": "^8.3.0" plugin START ###############################
4169
+
4170
+
4171
+ const filePath = path.join(
4172
+ __dirname,
4173
+ "../node_modules/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java"
4174
+ );
4175
+
4176
+ const SYSTEM_BARS_PATCH_MARKER = "CODEPLAY_KEYBOARD_INSET_FIX";
4177
+
4178
+ function getSystemBarsPatchedBlock(originalBlock, content) {
4179
+ const isCapacitor842OrNewer = content.includes("private String insetsHandling = INSETS_HANDLING_CSS;");
4180
+ const safeAreaCondition = isCapacitor842OrNewer
4181
+ ? "INSETS_HANDLING_CSS.equals(insetsHandling)"
4182
+ : "Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasViewportCover && insetHandlingEnabled";
4183
+
4184
+ return `if (shouldPassthroughInsets) {
4185
+ /* ${SYSTEM_BARS_PATCH_MARKER}
4186
+ ${originalBlock.split("\n").map(line => " " + line).join("\n")}
4187
+ */
4188
+
4189
+ // Codeplay fix: avoid adding IME height as WebView parent padding.
4190
+ v.setPadding(0, 0, 0, 0);
4191
+
4192
+ if (${safeAreaCondition}) {
4193
+ Insets safeAreaInsets = calcSafeAreaInsets(insets);
4194
+ injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
4195
+ }
4196
+
4197
+ return insets; // WebView handles keyboard and safe-area sizing.
4198
+ }`;
4199
+ }
4200
+
4201
+ function findShouldPassthroughInsetsBlock(content) {
4202
+ const start = content.indexOf(" if (shouldPassthroughInsets) {");
4203
+ if (start === -1) return null;
4204
+
4205
+ const endMarker = "\n\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM)";
4206
+ const end = content.indexOf(endMarker, start);
4207
+ if (end === -1) return null;
4208
+
4209
+ return {
4210
+ start,
4211
+ end,
4212
+ text: content.slice(start, end)
4213
+ };
4214
+ }
4215
+
4216
+ // 🚨 ERROR MESSAGE
4217
+ const ERROR_MSG = `
4218
+ ❌ Capacitor SystemBars.java structure changed!
4219
+
4220
+ Plugin: @capacitor/android
4221
+ Path : node_modules\@capacitor\android\capacitor\src\main\java\com\getcapacitor\plugin\SystemBars.java
4222
+
4223
+ Check version of "@capacitor/android": in package.json
4224
+
4225
+ 👉 Expected code block not found.
4226
+
4227
+ This usually means Capacitor updated internally.
4228
+
4229
+ Please:
4230
+ 1. Open SystemBars.java
4231
+ 2. Update patch script
4232
+ 3. Re-run build
4233
+
4234
+ ⛔ Build stopped.
4235
+ `;
4236
+
4237
+ function patchFile() {
4238
+ if (!fs.existsSync(filePath)) {
4239
+ console.error("❌ SystemBars.java not found!");
4240
+ process.exit(1);
4241
+ }
4242
+
4243
+ const capacitorConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
4244
+ if (capacitorConfig.plugins?.SystemBars?.insetsHandling === "disable") {
4245
+ console.log('ℹ️ SystemBars.insetsHandling is "disable"; skipping SystemBars.java keyboard inset patch.');
4246
+ return;
4247
+ }
4248
+
4249
+ let content = fs.readFileSync(filePath, "utf8");
4250
+
4251
+ // ✅ Already patched?
4252
+ if (content.includes(SYSTEM_BARS_PATCH_MARKER) || content.includes("🔴 ORIGINAL CODE (COMMENTED FOR SAFETY)")) {
4253
+ console.log("✅ Already SystemBars.java patched. Skipping...");
4254
+ return;
4255
+ }
4256
+
4257
+ const block = findShouldPassthroughInsetsBlock(content);
4258
+ if (!block) {
4259
+ console.error(ERROR_MSG);
4260
+ process.exit(1);
4261
+ }
4262
+
4263
+ // 🔁 Replace
4264
+ const updated =
4265
+ content.slice(0, block.start) +
4266
+ getSystemBarsPatchedBlock(block.text, content) +
4267
+ content.slice(block.end);
4268
+
4269
+ fs.writeFileSync(filePath, updated, "utf8");
4270
+
4271
+ console.log("✅ SystemBars.java patched with Codeplay keyboard inset fix!");
4272
+ }
4273
+
4274
+ //patchFile();
4275
+
4276
+ //################################## SystemBars.java update for "@capacitor/android": "^8.3.0" plugin END ###############################
4277
+
4278
+ // Android build optimization START
4279
+ function findAndroidBuildBlock(content, name) {
4280
+ // Mask comments and strings without changing offsets or line endings.
4281
+ const masked = content.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g,
4282
+ value => value.replace(/[^\r\n]/g, ' '));
4283
+ const header = new RegExp(`\\b${name}\\s*\\{`, 'g');
4284
+ let depth = 0;
4285
+ for (let index = 0; index < masked.length; index += 1) {
4286
+ if (depth === 0) {
4287
+ header.lastIndex = index;
4288
+ const match = header.exec(masked);
4289
+ if (match && match.index === index) {
4290
+ const start = header.lastIndex;
4291
+ let blockDepth = 1;
4292
+ for (let end = start; end < masked.length; end += 1) {
4293
+ if (masked[end] === '{') blockDepth += 1;
4294
+ if (masked[end] === '}' && --blockDepth === 0) return { start, end };
4295
+ }
4296
+ throw new Error(`Unclosed Android Gradle ${name} block.`);
4297
+ }
4298
+ }
4299
+ if (masked[index] === '{') depth += 1;
4300
+ if (masked[index] === '}') depth -= 1;
4301
+ }
4302
+ return null;
4303
+ }
4304
+
4305
+ function syncAndroidBuildOptimization() {
4306
+ if (buildPlatform !== 'android' || !fs.existsSync(androidAppBuildGradlePath)) return;
4307
+
4308
+ const original = fs.readFileSync(androidAppBuildGradlePath, 'utf8');
4309
+ const newline = original.includes('\r\n') ? '\r\n' : '\n';
4310
+ const androidBlock = findAndroidBuildBlock(original, 'android');
4311
+ if (!androidBlock) throw new Error('Android Gradle android block was not found.');
4312
+ let androidContent = original.slice(androidBlock.start, androidBlock.end);
4313
+ const buildTypesBlock = findAndroidBuildBlock(androidContent, 'buildTypes');
4314
+ if (!buildTypesBlock) throw new Error('Android Gradle buildTypes block was not found.');
4315
+ let buildTypesContent = androidContent.slice(buildTypesBlock.start, buildTypesBlock.end);
4316
+
4317
+ // Match the existing finalrelease compatibility exception for this native plugin.
4318
+ const ffmpegPluginXml = path.join(process.cwd(), 'node_modules', 'codeplay-fmpg', 'plugin.xml');
4319
+ const optimizeRelease = !(fs.existsSync(ffmpegPluginXml)
4320
+ && /<plugin\b/.test(fs.readFileSync(ffmpegPluginXml, 'utf8')));
4321
+
4322
+ for (const name of ['debug', 'release']) {
4323
+ const enabled = name === 'debug' || optimizeRelease;
4324
+ const block = findAndroidBuildBlock(buildTypesContent, name);
4325
+ const blockComments = buildTypesContent.match(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g) || [];
4326
+ if (!block && blockComments.some(comment => (comment.startsWith('//') || comment.startsWith('/*'))
4327
+ && new RegExp(`\\b${name}\\s*\\{`).test(comment))) continue;
4328
+ let body = block ? buildTypesContent.slice(block.start, block.end) : `${newline} `;
4329
+ const comments = (body.match(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g) || [])
4330
+ .filter(value => value.startsWith('//') || value.startsWith('/*'));
4331
+ for (const setting of ['minifyEnabled', 'shrinkResources']) {
4332
+ const masked = body.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g,
4333
+ value => value.replace(/[^\r\n]/g, ' '));
4334
+ const settingPattern = new RegExp(`\\b${setting}\\b`);
4335
+ // Existing values, expressions and commented opt-outs take precedence.
4336
+ if (settingPattern.test(masked) || comments.some(comment => settingPattern.test(comment))) continue;
4337
+ const value = setting === 'shrinkResources'
4338
+ ? enabled && /\bminifyEnabled\s*(?:=\s*)?true\b/.test(masked)
4339
+ : enabled;
4340
+ body = `${newline} ${setting} ${value}${body}`;
4341
+ }
4342
+ const activeBody = body.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g,
4343
+ value => value.startsWith('//') || value.startsWith('/*') ? value.replace(/[^\r\n]/g, ' ') : value);
4344
+ const defaultRules = [...activeBody.matchAll(/getDefaultProguardFile\(\s*(['"])proguard-android\.txt\1\s*\)/g)];
4345
+ for (const match of defaultRules.reverse()) {
4346
+ body = body.slice(0, match.index) + "getDefaultProguardFile('proguard-android-optimize.txt')"
4347
+ + body.slice(match.index + match[0].length);
4348
+ }
4349
+ if (!/\bproguardFiles\b/.test(activeBody)
4350
+ && !comments.some(comment => /\bproguardFiles\b/.test(comment))) {
4351
+ body = `${newline} proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'${body}`;
4352
+ }
4353
+ buildTypesContent = block
4354
+ ? buildTypesContent.slice(0, block.start) + body + buildTypesContent.slice(block.end)
4355
+ : `${buildTypesContent.trimEnd()}${newline} ${name} {${body}}${newline} `;
4356
+ }
4357
+
4358
+ androidContent = androidContent.slice(0, buildTypesBlock.start) + buildTypesContent
4359
+ + androidContent.slice(buildTypesBlock.end);
4360
+ const next = original.slice(0, androidBlock.start) + androidContent + original.slice(androidBlock.end);
4361
+ if (next !== original) fs.writeFileSync(androidAppBuildGradlePath, next, 'utf8');
4362
+
4363
+ const propertiesPath = path.join(androidPlatformPath, 'gradle.properties');
4364
+ const properties = fs.existsSync(propertiesPath) ? fs.readFileSync(propertiesPath, 'utf8') : '';
4365
+ const property = 'android.r8.optimizedResourceShrinking=true';
4366
+ const propertyPattern = /^[ \t]*[#!]?[ \t]*android\.r8\.optimizedResourceShrinking(?:[ \t]*[=:]|[ \t]+|[ \t]*$)/m;
4367
+ const nextProperties = propertyPattern.test(properties)
4368
+ ? properties
4369
+ : `${properties.trimEnd()}${newline}${property}${newline}`;
4370
+ if (nextProperties !== properties) fs.writeFileSync(propertiesPath, nextProperties, 'utf8');
4371
+ console.log('✅ Android build optimization defaults checked; existing settings and commented opt-outs preserved.');
4372
+ }
4373
+ // Android build optimization END
4374
+
4375
+ function syncAndroidCordovaResourceKeep() {
4376
+ if (buildPlatform !== 'android' || !fs.existsSync(androidPlatformPath)) return;
4377
+
4378
+ // Cordova loads config.xml by name; preserve it even when no plugins are installed yet.
4379
+ const rawPath = path.join(androidPlatformPath, 'app', 'src', 'main', 'res', 'raw');
4380
+ const keepPath = path.join(rawPath, 'codeplay_cordova_keep.xml');
4381
+ const content = '<?xml version="1.0" encoding="utf-8"?>\n'
4382
+ + '<resources xmlns:tools="http://schemas.android.com/tools"\n'
4383
+ + ' tools:keep="@xml/config" />\n';
4384
+ if (fs.existsSync(keepPath) && fs.readFileSync(keepPath, 'utf8') === content) return;
4385
+
4386
+ fs.mkdirSync(rawPath, { recursive: true });
4387
+ fs.writeFileSync(keepPath, content, 'utf8');
4388
+ }
4389
+
4390
+ async function main() {
4391
+ validateThemeFolderLocation();
4392
+ validateAndRestoreSignDetails();
4393
+ ensureMandatoryCapacitorPlugins();
4394
+
4395
+ await loadPluginVersions(); // 🔥 NEW
4396
+ await syncAgentInstructions();
4397
+
4398
+ await syncAdmobNextgenMigration();
4399
+ syncCodeplaySystemBarsPackage();
4400
+ syncSystemBarsSafeAreaMigration();
4401
+ await checkPlugins();
4402
+ ensureIapStartupOnHomePage();
4403
+ syncRevenueCatIapSetup();
4404
+ syncFirebaseAnalyticsSetup();
4405
+ checkAndupdateDropInViteConfig();
4406
+ checkAdmobConfigurationProperty();
4407
+ syncAndroidAdIdPermission();
4408
+ syncAndroidBuildOptimization();
4409
+ syncAndroidCordovaResourceKeep();
4410
+ }
4411
+
4412
+ await main();
4413
+ })().catch(err => {
4414
+ console.error(err?.message || err);
4415
+ process.exit(1);
4416
+ });
4417
+
4418
+
4419
+
4420
+
4421
+ /*
4422
+ Release Notes
4423
+
4424
+ 6.5
4425
+ Enable debug/release optimization and optimized resource shrinking before Android builds.
4426
+ Preserve existing values and commented opt-outs without adding duplicate settings.
4427
+ Kids ad configuration disables iOS ATT prompts and excludes Android AD_ID during builds.
4428
+ Non-kids apps retain tracking prompts and normal permission behavior.
4429
+ Ad configuration migration preserves newer local configuration versions.
4430
+
4431
+ 5.1
4432
+ Kotlin version update is commented. Previously admob is not worked if not update the kotlin version to higher version
4433
+
4434
+ */