codeplay-common 4.9.9 → 4.10.0

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