@saltcorn/mobile-builder 1.6.3-beta.1 → 1.7.0-alpha.1

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.
@@ -20,9 +20,10 @@ exports.prepareSplashIcon = prepareSplashIcon;
20
20
  exports.prepareExportOptionsPlist = prepareExportOptionsPlist;
21
21
  exports.modifyInfoPlist = modifyInfoPlist;
22
22
  exports.writeEntitlementsPlist = writeEntitlementsPlist;
23
- exports.runAddEntitlementsScript = runAddEntitlementsScript;
23
+ exports.injectCodeSignEntitlements = injectCodeSignEntitlements;
24
24
  exports.copyShareExtFiles = copyShareExtFiles;
25
25
  exports.modifyShareViewController = modifyShareViewController;
26
+ exports.createShareExtensionTarget = createShareExtensionTarget;
26
27
  exports.modifyAppDelegate = modifyAppDelegate;
27
28
  exports.writePrivacyInfo = writePrivacyInfo;
28
29
  exports.copyServerFiles = copyServerFiles;
@@ -76,6 +77,7 @@ function prepareBuildDir(buildDir, templateDir, pushEnabled, backgroundFetchEnab
76
77
  "@capacitor/core@7.4.5",
77
78
  "@capacitor/assets@3.0.5",
78
79
  "@capacitor/filesystem@7.1.6",
80
+ "@capacitor/file-transfer@1.0.12",
79
81
  "@capacitor/camera@7.0.3",
80
82
  "@capacitor/network@7.0.3",
81
83
  "@capacitor-community/sqlite@7.0.3",
@@ -107,6 +109,11 @@ function prepareBuildDir(buildDir, templateDir, pushEnabled, backgroundFetchEnab
107
109
  maxBuffer: 1024 * 1024 * 10,
108
110
  });
109
111
  }
112
+ /**
113
+ * Write `capacitor.config.ts` into the build directory.
114
+ * @param buildDir directory where the app will be built
115
+ * @param config capacitor build settings
116
+ */
110
117
  function writeCapacitorConfig(buildDir, config) {
111
118
  const cfgFile = (0, path_1.join)(buildDir, "capacitor.config.ts");
112
119
  const content = `
@@ -116,9 +123,8 @@ const config: CapacitorConfig = {
116
123
  appId: '${config.appId ? config.appId : "com.saltcorn.mobile.app"}',
117
124
  appName: '${config.appName ? config.appName : "SaltcornMobileApp"}',
118
125
  webDir: "www",
119
- ios: {
120
- scheme: "SaltcornMobileApp",
121
- },
126
+ // ios.limitsNavigationsToAppBoundDomains was dropped - it caused noisy
127
+ // script-injection warnings and wasn't needed for the cookie fix to work.
122
128
  android: {
123
129
  buildOptions: {
124
130
  ${config.keystorePath && config.buildType === "release"
@@ -130,15 +136,21 @@ const config: CapacitorConfig = {
130
136
  : ""}
131
137
  releaseType: '${config.buildType === "release" ? "AAB" : "APK"}',
132
138
  },
133
- ${config.unsecureNetwork ? "allowMixedContent: true," : ""}
134
139
  },
135
- ${config.unsecureNetwork
136
- ? `server: {
137
- cleartext: true,
138
- androidScheme: 'http',
139
- },`
140
- : ""}
140
+ server: {
141
+ // App's origin on iOS - must match MOBILE_APP_ORIGINS in server/app.js.
142
+ iosScheme: 'SaltcornMobileApp',
143
+ },
141
144
  plugins: {
145
+ // Needed together with WKAppBoundDomains (see modifyInfoPlist) to
146
+ // exempt the remote server's session cookie from iOS's third-party
147
+ // cookie blocking.
148
+ CapacitorHttp: {
149
+ enabled: true,
150
+ },
151
+ CapacitorCookies: {
152
+ enabled: true,
153
+ },
142
154
  CapacitorSQLite: {
143
155
  iosDatabaseLocation: 'Library/CapacitorDatabase',
144
156
  iosIsEncryption: true,
@@ -165,6 +177,11 @@ const config: CapacitorConfig = {
165
177
  export default config;`;
166
178
  (0, fs_extra_1.writeFileSync)(cfgFile, content);
167
179
  }
180
+ /**
181
+ * Copy the app icon into all required asset slots (icon, splash variants).
182
+ * @param buildDir directory where the app will be built
183
+ * @param appIcon path to the source PNG
184
+ */
168
185
  function prepAppIcon(buildDir, appIcon) {
169
186
  for (const icon of [
170
187
  "icon-only",
@@ -178,6 +195,11 @@ function prepAppIcon(buildDir, appIcon) {
178
195
  });
179
196
  }
180
197
  }
198
+ /**
199
+ * Returns the Android permissions required for the build.
200
+ * @param allowFCM include FCM/push notification permissions
201
+ * @returns array of Android permission strings
202
+ */
181
203
  function androidPermissions(allowFCM) {
182
204
  const state = (0, state_1.getState)();
183
205
  if (!state)
@@ -199,6 +221,10 @@ function androidPermissions(allowFCM) {
199
221
  }
200
222
  return Array.from(permissions);
201
223
  }
224
+ /**
225
+ * Returns the Android hardware features required for the build.
226
+ * @returns array of Android feature strings
227
+ */
202
228
  function androidFeatures() {
203
229
  const state = (0, state_1.getState)();
204
230
  if (!state)
@@ -211,7 +237,14 @@ function androidFeatures() {
211
237
  }
212
238
  return Array.from(features);
213
239
  }
214
- async function modifyAndroidManifest(buildDir, allowShareTo, allowFCM, allowAuthIntent, allowClearTextTraffic) {
240
+ /**
241
+ * Patch `AndroidManifest.xml` with permissions, features, and optional intents.
242
+ * @param buildDir directory where the app will be built
243
+ * @param allowShareTo add share-to intent filter
244
+ * @param allowFCM add FCM push notification support
245
+ * @param allowAuthIntent add authentication intent
246
+ */
247
+ async function modifyAndroidManifest(buildDir, allowShareTo, allowFCM, allowAuthIntent) {
215
248
  console.log("modifyAndroidManifest");
216
249
  try {
217
250
  const androidManifest = (0, path_1.join)(buildDir, "android", "app", "src", "main", "AndroidManifest.xml");
@@ -229,9 +262,6 @@ async function modifyAndroidManifest(buildDir, allowShareTo, allowFCM, allowAuth
229
262
  "android:fullBackupContent": "false",
230
263
  "android:dataExtractionRules": "@xml/data_extraction_rules",
231
264
  "android:networkSecurityConfig": "@xml/network_security_config",
232
- ...(allowClearTextTraffic
233
- ? { "android:usesCleartextTraffic": "true" }
234
- : {}),
235
265
  };
236
266
  if (allowFCM) {
237
267
  parsed.manifest.application[0]["meta-data"] = [
@@ -302,6 +332,11 @@ async function modifyAndroidManifest(buildDir, allowShareTo, allowFCM, allowAuth
302
332
  console.log(`Unable to modify the AndroidManifest.xml: ${error.message ? error.message : "Unknown error"}`);
303
333
  }
304
334
  }
335
+ /**
336
+ * Returns true if any of the included plugins exposes an authentication method.
337
+ * @param plugins list of included plugin names
338
+ * @returns whether an auth method is available
339
+ */
305
340
  function hasAuthMethod(plugins) {
306
341
  const state = (0, state_1.getState)();
307
342
  for (const pluginName of plugins) {
@@ -311,6 +346,10 @@ function hasAuthMethod(plugins) {
311
346
  }
312
347
  return false;
313
348
  }
349
+ /**
350
+ * Write the Android data extraction rules XML (excludes app data from backups).
351
+ * @param buildDir directory where the app will be built
352
+ */
314
353
  function writeDataExtractionRules(buildDir) {
315
354
  console.log("writeDataExtractionRules");
316
355
  const dataExtractionRules = (0, path_1.join)(buildDir, "android", "app", "src", "main", "res", "xml", "data_extraction_rules.xml");
@@ -331,6 +370,11 @@ function writeDataExtractionRules(buildDir) {
331
370
  </data-extraction-rules>
332
371
  `);
333
372
  }
373
+ /**
374
+ * Copy the pre-populated SQLite database into the platform asset directories.
375
+ * @param buildDir directory where the app will be built
376
+ * @param platforms target platforms (`"android"` / `"ios"`)
377
+ */
334
378
  function copyPrepopulatedDb(buildDir, platforms) {
335
379
  console.log("copyPrepopulatedDb", buildDir, platforms);
336
380
  if (platforms.includes("android")) {
@@ -340,6 +384,11 @@ function copyPrepopulatedDb(buildDir, platforms) {
340
384
  (0, fs_extra_1.copySync)((0, path_1.join)(buildDir, "www", "scdb.sqlite"), (0, path_1.join)(buildDir, "ios", "App", "App", "public", "assets", "databases", "prepopulated.db"));
341
385
  }
342
386
  }
387
+ /**
388
+ * Extract the bare domain name from a URL (strips protocol, port, trailing slash).
389
+ * @param url full server URL
390
+ * @returns domain string
391
+ */
343
392
  function extractDomain(url) {
344
393
  let domain = url;
345
394
  if (domain.startsWith("http://"))
@@ -352,17 +401,26 @@ function extractDomain(url) {
352
401
  domain = domain.substring(0, domain.indexOf(":"));
353
402
  return domain;
354
403
  }
355
- function writeNetworkSecurityConfig(buildDir, serverPath) {
404
+ /**
405
+ * Write the Android network security config. HTTPS is required, so this is
406
+ * always the restrictive (no cleartext) version.
407
+ * @param buildDir directory where the app will be built
408
+ */
409
+ function writeNetworkSecurityConfig(buildDir) {
356
410
  console.log("writeNetworkSecurityConfig");
357
411
  const networkSecurityConfig = (0, path_1.join)(buildDir, "android", "app", "src", "main", "res", "xml", "network_security_config.xml");
358
- (0, fs_extra_1.writeFileSync)(networkSecurityConfig, `<?xml version="1.0" encoding="utf-8"?>
412
+ const content = `<?xml version="1.0" encoding="utf-8"?>
359
413
  <network-security-config>
360
- <domain-config cleartextTrafficPermitted="true">
361
- <domain includeSubdomains="true">${extractDomain(serverPath)}</domain>
362
- </domain-config>
363
- </network-security-config>
364
- `);
414
+ <base-config cleartextTrafficPermitted="false" />
415
+ </network-security-config>`;
416
+ (0, fs_extra_1.writeFileSync)(networkSecurityConfig, content);
365
417
  }
418
+ /**
419
+ *
420
+ * Patch `config.xml` with app name, app ID, and version.
421
+ * @param buildDir directory where the app will be built
422
+ * @param config object with optional `appName`, `appId`, and `appVersion` fields
423
+ */
366
424
  async function modifyConfigXml(buildDir, config) {
367
425
  try {
368
426
  const configXml = (0, path_1.join)(buildDir, "config.xml");
@@ -425,6 +483,11 @@ async function prepareSplashIcon(buildDir, splashIcon, platforms) {
425
483
  console.log(`Unable to set the splash icon '${splashIcon}': ${error.message ? error.message : "Unknown error"}`);
426
484
  }
427
485
  }
486
+ /**
487
+ * Generate the iOS `AppIcon.appiconset` with all required sizes.
488
+ * @param buildDir directory where the app will be built
489
+ * @param appIcon path to the source PNG
490
+ */
428
491
  async function prepareAppIconSet(buildDir, appIcon) {
429
492
  console.log("prepareAppIconSet", buildDir, appIcon);
430
493
  const dir = (0, path_1.join)(buildDir, "AppIcon.appiconset");
@@ -463,6 +526,10 @@ async function prepareAppIconSet(buildDir, appIcon) {
463
526
  console.log(`Unable to generate appicon set for '${appIcon}': ${error.message ? error.message : "Unknown error"}`);
464
527
  }
465
528
  }
529
+ /**
530
+ * Write `ExportOptions.plist` used by `xcodebuild -exportArchive`.
531
+ * @param param0 buildDir, appId, and iOS provisioning parameters
532
+ */
466
533
  function prepareExportOptionsPlist({ buildDir, appId, iosParams }) {
467
534
  console.log("prepareExportOptionsPlist", buildDir, appId, iosParams);
468
535
  const buildShareExtBloock = () => {
@@ -474,12 +541,13 @@ function prepareExportOptionsPlist({ buildDir, appId, iosParams }) {
474
541
  <string>${iosParams.shareExtensionProvisioningProfile.guuid}</string>`;
475
542
  };
476
543
  try {
544
+ const method = iosParams.isAdHoc ? "ad-hoc" : "app-store-connect";
477
545
  (0, fs_extra_1.writeFileSync)((0, path_1.join)(buildDir, "ExportOptions.plist"), `<?xml version="1.0" encoding="UTF-8"?>
478
546
  <!DOCTYPE plist PUBLIC "~//Apple/DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
479
547
  <plist version="1.0">
480
548
  <dict>
481
549
  <key>method</key>
482
- <string>app-store-connect</string>
550
+ <string>${method}</string>
483
551
  <key>provisioningProfiles</key>
484
552
  <dict>
485
553
  <key>${appId}</key>
@@ -494,26 +562,52 @@ function prepareExportOptionsPlist({ buildDir, appId, iosParams }) {
494
562
  console.log(`Unable to write the ExportOptionsPlist file: ${error.message ? error.message : "Unknown error"}`);
495
563
  }
496
564
  }
497
- function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSyncEnabled, allowClearTextTraffic) {
565
+ /**
566
+ * Patch `Info.plist` with background modes, usage descriptions, and optional URL schemes.
567
+ * @param buildDir directory where the app will be built
568
+ * @param allowShareTo add URL scheme for share-to integration
569
+ * @param backgroundSyncEnabled background sync task is scheduled
570
+ * @param pushSyncEnabled add remote-notification background mode
571
+ * @param backgroundFetchEnabled background.js is included (sync or heartbeat configured)
572
+ * @param serverURL server URL, added to WKAppBoundDomains for the iOS cookie
573
+ * fix - changing servers (e.g. a new ngrok URL) needs a rebuild to apply.
574
+ */
575
+ function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSyncEnabled, backgroundFetchEnabled = false, serverURL) {
498
576
  const infoPlist = (0, path_1.join)(buildDir, "ios", "App", "App", "Info.plist");
499
577
  const content = (0, fs_extra_1.readFileSync)(infoPlist, "utf8");
578
+ let appBoundHostname;
579
+ if (serverURL) {
580
+ try {
581
+ appBoundHostname = new URL(serverURL).hostname;
582
+ }
583
+ catch {
584
+ // invalid/missing URL - skip WKAppBoundDomains rather than write a broken plist
585
+ }
586
+ }
587
+ const backgroundModes = [
588
+ ...(backgroundFetchEnabled ? ["fetch", "processing"] : []),
589
+ ...(pushSyncEnabled ? ["remote-notification"] : []),
590
+ ];
591
+ const taskIds = [
592
+ "com.transistorsoft.fetch",
593
+ ...(backgroundSyncEnabled ? ["com.transistorsoft.background_sync"] : []),
594
+ ...(backgroundFetchEnabled
595
+ ? ["com.transistorsoft.push_sync_heartbeat"]
596
+ : []),
597
+ ];
500
598
  const newCfgs = `
501
- ${backgroundSyncEnabled
599
+ ${backgroundFetchEnabled
502
600
  ? `<key>BGTaskSchedulerPermittedIdentifiers</key>
503
601
  <array>
504
- <string>com.transistorsoft.fetch</string>
505
- </array>
506
- <key>UIBackgroundModes</key>
507
- <array>
508
- <string>fetch</string>
602
+ ${taskIds.map((id) => `<string>${id}</string>`).join("\n ")}
509
603
  </array>
510
604
  `
511
605
  : ""}
512
606
 
513
- ${pushSyncEnabled
607
+ ${backgroundModes.length > 0
514
608
  ? `<key>UIBackgroundModes</key>
515
609
  <array>
516
- <string>remote-notification</string>
610
+ ${backgroundModes.map((m) => `<string>${m}</string>`).join("\n ")}
517
611
  </array>
518
612
  `
519
613
  : ""}
@@ -531,6 +625,12 @@ function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSync
531
625
  <true/>
532
626
  <key>LSSupportsOpeningDocumentsInPlace</key>
533
627
  <true/>
628
+ ${appBoundHostname
629
+ ? `<key>WKAppBoundDomains</key>
630
+ <array>
631
+ <string>${appBoundHostname}</string>
632
+ </array>`
633
+ : ""}
534
634
  ${allowShareTo
535
635
  ? `<key>CFBundleURLTypes</key>
536
636
  <array>
@@ -546,29 +646,38 @@ function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSync
546
646
  </dict>
547
647
  </array>`
548
648
  : ""}
549
- ${allowClearTextTraffic
550
- ? `<key>NSAppTransportSecurity</key>
551
- <dict>
552
- <key>NSAllowsArbitraryLoads</key>
553
- <true/>
554
- </dict>`
555
- : ""}
556
649
  `;
557
650
  // add newCfgs after the first <dict> tag
558
651
  const newContent = content.replace(/<dict>/, `<dict>${newCfgs}`);
559
652
  (0, fs_extra_1.writeFileSync)(infoPlist, newContent, "utf8");
560
653
  }
561
- function writeEntitlementsPlist(buildDir) {
654
+ /**
655
+ * Write `App.entitlements` with the optional APNs environment and app group.
656
+ * @param buildDir directory where the app will be built
657
+ * @param buildType `"debug"` → development APNs, `"release"` → production APNs
658
+ * @param pushSync include the APNs environment entitlement
659
+ * @param appGroupId optional app group ID for share extension data sharing
660
+ */
661
+ function writeEntitlementsPlist(buildDir, buildType, pushSync, appGroupId) {
562
662
  const file = (0, path_1.join)(buildDir, "ios", "App", "App", "App.entitlements");
663
+ const apsEnv = buildType === "release" ? "production" : "development";
664
+ const apsEnvEntry = pushSync
665
+ ? ` <key>aps-environment</key>
666
+ <string>${apsEnv}</string>`
667
+ : "";
668
+ const appGroupsEntry = appGroupId
669
+ ? ` <key>com.apple.security.application-groups</key>
670
+ <array>
671
+ <string>${appGroupId}</string>
672
+ </array>`
673
+ : "";
563
674
  try {
564
675
  (0, fs_extra_1.writeFileSync)(file, `<?xml version="1.0" encoding="UTF-8"?>
565
676
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
566
677
  <plist version="1.0">
567
678
  <dict>
568
- <key>aps-environment</key>
569
- <string>production</string>
570
- <key>com.apple.security.application-groups</key>
571
- <array />
679
+ ${apsEnvEntry}
680
+ ${appGroupsEntry}
572
681
  </dict>
573
682
  </plist>`);
574
683
  }
@@ -576,23 +685,45 @@ function writeEntitlementsPlist(buildDir) {
576
685
  console.log(`Unable to write the Entitlements plist file: ${error.message ? error.message : "Unknown error"}`);
577
686
  }
578
687
  }
579
- function runAddEntitlementsScript(buildDir) {
688
+ /**
689
+ * Inject `CODE_SIGN_ENTITLEMENTS` into `project.pbxproj` for the App target.
690
+ * Uses `INFOPLIST_FILE = App/Info.plist;` as an anchor (unique to the App target).
691
+ * Idempotent — skips if the key is already present.
692
+ * @param buildDir directory where the app will be built
693
+ */
694
+ function injectCodeSignEntitlements(buildDir) {
695
+ const pbxprojPath = (0, path_1.join)(buildDir, "ios", "App", "App.xcodeproj", "project.pbxproj");
580
696
  try {
581
- const result = (0, child_process_1.spawnSync)("ruby", ["add_entitlements.rb"], {
582
- cwd: buildDir,
583
- });
584
- console.log(result.output.toString());
697
+ let content = (0, fs_extra_1.readFileSync)(pbxprojPath, "utf8");
698
+ // check for this exact insertion, not just any CODE_SIGN_ENTITLEMENTS -
699
+ // the share-ext target sets its own, which isn't the same thing
700
+ if (content.includes("CODE_SIGN_ENTITLEMENTS = App/App.entitlements;")) {
701
+ console.log("CODE_SIGN_ENTITLEMENTS already set in project.pbxproj");
702
+ return;
703
+ }
704
+ content = content.replaceAll("INFOPLIST_FILE = App/Info.plist;", `CODE_SIGN_ENTITLEMENTS = App/App.entitlements;\n\t\t\t\tINFOPLIST_FILE = App/Info.plist;`);
705
+ (0, fs_extra_1.writeFileSync)(pbxprojPath, content, "utf8");
706
+ console.log("CODE_SIGN_ENTITLEMENTS set in project.pbxproj for App target");
585
707
  }
586
708
  catch (error) {
587
- console.log(`Unable to run the add_entitlements.rb script: ${error.message ? error.message : "Unknown error"}`);
709
+ console.log(`Unable to set CODE_SIGN_ENTITLEMENTS in project.pbxproj: ${error.message ?? "Unknown error"}`);
588
710
  }
589
711
  }
712
+ /**
713
+ * Copy share extension source files into the iOS project directory.
714
+ * @param buildDir directory where the app will be built
715
+ */
590
716
  function copyShareExtFiles(buildDir) {
591
717
  const iosAppDir = (0, path_1.join)(buildDir, "ios", "App");
592
718
  const sefDir = (0, path_1.join)(buildDir, "share_extension_files");
593
719
  (0, fs_extra_1.copySync)((0, path_1.join)(sefDir, "ShareViewController.swift"), (0, path_1.join)(iosAppDir, "share-ext", "ShareViewController.swift"), { overwrite: true });
594
720
  (0, fs_extra_1.copySync)((0, path_1.join)(sefDir, "Info.plist"), (0, path_1.join)(iosAppDir, "share-ext", "Info.plist"), { overwrite: true });
595
721
  }
722
+ /**
723
+ * Replace `YOUR_APP_GROUP_ID` placeholder in `ShareViewController.swift`.
724
+ * @param buildDir directory where the app will be built
725
+ * @param groupId app group identifier
726
+ */
596
727
  function modifyShareViewController(buildDir, groupId) {
597
728
  const shareVCFile = (0, path_1.join)(buildDir, "ios", "App", "share-ext", "ShareViewController.swift");
598
729
  let content = (0, fs_extra_1.readFileSync)(shareVCFile, "utf8");
@@ -600,15 +731,68 @@ function modifyShareViewController(buildDir, groupId) {
600
731
  content = content.replace(/YOUR_APP_GROUP_ID/g, groupId);
601
732
  (0, fs_extra_1.writeFileSync)(shareVCFile, content, "utf8");
602
733
  }
603
- function modifyAppDelegate(buildDir, backgroundSyncEnabled, pushSyncEnabled, allowShareTo) {
734
+ /**
735
+ * Write the entitlements plist for the share extension target (app group
736
+ * only - no APNs entry, extensions don't receive push notifications).
737
+ * @param buildDir directory where the app will be built
738
+ * @param appGroupId app group ID shared with the main app
739
+ * @returns path to the entitlements file, relative to `ios/App`
740
+ */
741
+ function writeShareExtEntitlementsPlist(buildDir, appGroupId) {
742
+ const relPath = (0, path_1.join)("share-ext", "ShareExtension.entitlements");
743
+ const file = (0, path_1.join)(buildDir, "ios", "App", relPath);
744
+ (0, fs_extra_1.writeFileSync)(file, `<?xml version="1.0" encoding="UTF-8"?>
745
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
746
+ <plist version="1.0">
747
+ <dict>
748
+ <key>com.apple.security.application-groups</key>
749
+ <array>
750
+ <string>${appGroupId}</string>
751
+ </array>
752
+ </dict>
753
+ </plist>`);
754
+ return relPath;
755
+ }
756
+ /**
757
+ * Add a "share-ext" Share Extension target to `App.xcodeproj`, so the app
758
+ * can be built with iOS share-to support without ever opening Xcode.
759
+ * @param buildDir directory where the app will be built
760
+ * @param appId main app's bundle id - the extension's is `${appId}.share-ext`
761
+ * @param appGroupId optional app group ID, wired into a matching
762
+ * entitlements plist for the new target
763
+ */
764
+ function createShareExtensionTarget(buildDir, appId, appGroupId) {
765
+ const iosAppDir = (0, path_1.join)(buildDir, "ios", "App");
766
+ const projectPath = (0, path_1.join)(iosAppDir, "App.xcodeproj");
767
+ const entitlementsRelPath = appGroupId
768
+ ? writeShareExtEntitlementsPlist(buildDir, appGroupId)
769
+ : undefined;
770
+ const scriptPath = (0, path_1.join)(__dirname, "xcode", "create_share_extension.rb");
771
+ const args = [scriptPath, projectPath, `${appId}.share-ext`];
772
+ if (entitlementsRelPath)
773
+ args.push(entitlementsRelPath);
774
+ const result = (0, child_process_1.spawnSync)("ruby", args, { cwd: iosAppDir });
775
+ if (result.status !== 0) {
776
+ throw new Error(`Unable to create the share extension target: ${result.stderr?.toString() || result.error?.message || "unknown error"}`);
777
+ }
778
+ console.log(result.stdout?.toString());
779
+ }
780
+ /**
781
+ * Patch `AppDelegate.swift` to register background tasks and push handlers.
782
+ * @param buildDir directory where the app will be built
783
+ * @param pushSyncEnabled register remote-notification handler
784
+ * @param allowShareTo register URL scheme handler for share-to
785
+ * @param backgroundFetchEnabled initialize TSBackgroundFetch (sync or heartbeat)
786
+ */
787
+ function modifyAppDelegate(buildDir, pushSyncEnabled, allowShareTo, backgroundFetchEnabled = false) {
604
788
  const appDelegateFile = (0, path_1.join)(buildDir, "ios", "App", "App", "AppDelegate.swift");
605
789
  let content = (0, fs_extra_1.readFileSync)(appDelegateFile, "utf8");
606
790
  // modify cusomization point after application launch
607
791
  content = content.replace(/func application\(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: \[UIApplication.LaunchOptionsKey: Any\]\?\) -> Bool {/, `func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
608
792
  // Override point for customization after application launch.
609
793
 
610
-
611
- ${backgroundSyncEnabled
794
+
795
+ ${backgroundFetchEnabled
612
796
  ? `// [capacitor-background-fetch]
613
797
  let fetchManager = TSBackgroundFetch.sharedInstance();
614
798
  fetchManager?.didFinishLaunching();`
@@ -616,7 +800,7 @@ function modifyAppDelegate(buildDir, backgroundSyncEnabled, pushSyncEnabled, all
616
800
 
617
801
  self.window?.rootViewController?.navigationController?.interactivePopGestureRecognizer?.isEnabled = false;
618
802
  `);
619
- if (backgroundSyncEnabled) {
803
+ if (backgroundFetchEnabled) {
620
804
  // add "import TSBackgroundFetch" before "@UIApplicationMain"
621
805
  content = content.replace(/@UIApplicationMain/, `import TSBackgroundFetch
622
806
 
@@ -729,7 +913,7 @@ function modifyAppDelegate(buildDir, backgroundSyncEnabled, pushSyncEnabled, all
729
913
  }
730
914
  (0, fs_extra_1.writeFileSync)(appDelegateFile, content, "utf8");
731
915
  }
732
- function writePrivacyInfo(buildDir, backgroundSyncEnabled) {
916
+ function writePrivacyInfo(buildDir, backgroundFetchEnabled) {
733
917
  const infoFile = (0, path_1.join)(buildDir, "ios", "App", "PrivacyInfo.xcprivacy");
734
918
  const content = `<?xml version="1.0" encoding="UTF-8"?>
735
919
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -747,7 +931,7 @@ function writePrivacyInfo(buildDir, backgroundSyncEnabled) {
747
931
  </array>
748
932
  </dict>
749
933
 
750
- ${backgroundSyncEnabled
934
+ ${backgroundFetchEnabled
751
935
  ? `
752
936
  <!-- [1] background_fetch: UserDefaults -->
753
937
  <dict>
@@ -776,7 +960,7 @@ function copyServerFiles(buildDir) {
776
960
  if (!(0, fs_extra_1.existsSync)(assetsDst)) {
777
961
  (0, fs_extra_1.mkdirSync)(assetsDst, { recursive: true });
778
962
  }
779
- const serverRoot = (0, path_1.join)(require.resolve("@saltcorn/server"), "..");
963
+ const serverRoot = (0, path_1.join)(require.resolve("@saltcorn/server/package.json"), "..");
780
964
  const srcPrefix = (0, path_1.join)(serverRoot, "public");
781
965
  const srcAssests = [
782
966
  "jquery-3.6.0.min.js",
@@ -835,7 +1019,7 @@ async function copySiteLogo(buildDir) {
835
1019
  * create a cfg file, the app use this configs
836
1020
  * @param param0
837
1021
  */
838
- function writeCfgFile({ buildDir, entryPoint, entryPointType, serverPath, synchedTables, tenantAppName, autoPublicLogin, showContinueAsPublicUser, allowOfflineMode, syncOnReconnect, syncOnAppResume, pushSync, syncInterval, allowShareTo, }) {
1022
+ function writeCfgFile({ buildDir, entryPoint, entryPointType, serverPath, synchedTables, tenantAppName, autoPublicLogin, showContinueAsPublicUser, allowOfflineMode, syncOnReconnect, syncOnAppResume, pushSync, syncInterval, pushSyncHeartbeatInterval, allowShareTo, apnsEnvironment, }) {
839
1023
  const wwwDir = (0, path_1.join)(buildDir, "www");
840
1024
  let cfg = {
841
1025
  version_tag: index_1.default.connectObj.version_tag,
@@ -851,7 +1035,9 @@ function writeCfgFile({ buildDir, entryPoint, entryPointType, serverPath, synche
851
1035
  syncOnAppResume,
852
1036
  pushSync,
853
1037
  syncInterval,
1038
+ pushSyncHeartbeatInterval,
854
1039
  allowShareTo,
1040
+ apnsEnvironment,
855
1041
  };
856
1042
  if (entryPointType !== "byrole") {
857
1043
  cfg.entry_point = `get/${entryPointType === "pagegroup" ? "page" : entryPointType}/${entryPoint}`;
@@ -975,7 +1161,7 @@ async function buildTablesFile(buildDir, includedPlugins) {
975
1161
  * @param buildDir directory where the app will be build
976
1162
  */
977
1163
  function copyTranslationFiles(buildDir) {
978
- const localesDir = (0, path_1.join)(require.resolve("@saltcorn/server"), "..", "locales");
1164
+ const localesDir = (0, path_1.join)(require.resolve("@saltcorn/server/package.json"), "..", "locales");
979
1165
  (0, fs_extra_1.copySync)(localesDir, (0, path_1.join)(buildDir, "www", "data", "locales"));
980
1166
  }
981
1167
  /**
@@ -1037,7 +1223,7 @@ async function prepareSplashPage(buildDir, pageName, serverUrl, tenantAppName, u
1037
1223
  title: page.title,
1038
1224
  body: !contents
1039
1225
  ? { above: [] }
1040
- : contents.above
1226
+ : "above" in contents
1041
1227
  ? contents
1042
1228
  : { above: [contents] },
1043
1229
  alerts: [],
@@ -1067,6 +1253,13 @@ async function prepareSplashPage(buildDir, pageName, serverUrl, tenantAppName, u
1067
1253
  console.log(error);
1068
1254
  }
1069
1255
  }
1256
+ /**
1257
+ * Write the iOS `Podfile` and run `pod install`.
1258
+ * @param buildDir directory where the app will be built
1259
+ * @param hasPush include push notification pods
1260
+ * @param hasBackgroundFetch include background fetch pod
1261
+ * @param hasSilentPush include silent-notifications pod
1262
+ */
1070
1263
  function writePodfile(buildDir, hasPush, hasBackgroundFetch, hasSilentPush) {
1071
1264
  const state = (0, state_1.getState)();
1072
1265
  let hasGeolocation = false;
@@ -1095,7 +1288,10 @@ function writePodfile(buildDir, hasPush, hasBackgroundFetch, hasSilentPush) {
1095
1288
  pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
1096
1289
  pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
1097
1290
  pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
1098
- ${hasGeolocation ? `pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation'` : ""}
1291
+ pod 'CapacitorFileTransfer', :path => '../../node_modules/@capacitor/file-transfer'
1292
+ ${hasGeolocation
1293
+ ? `pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation'`
1294
+ : ""}
1099
1295
  pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
1100
1296
  pod 'CapacitorScreenOrientation', :path => '../../node_modules/@capacitor/screen-orientation'
1101
1297
  pod 'SendIntent', :path => '../../node_modules/send-intent'
@@ -1105,8 +1301,12 @@ function writePodfile(buildDir, hasPush, hasBackgroundFetch, hasSilentPush) {
1105
1301
  ? `pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
1106
1302
  pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'`
1107
1303
  : ""}
1108
- ${hasSilentPush ? `pod 'CapacitorPluginSilentNotifications', :path => '../../node_modules/capacitor-plugin-silent-notifications'` : ""}
1109
- ${hasBackgroundFetch ? `pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../node_modules/@transistorsoft/capacitor-background-fetch'` : ""}
1304
+ ${hasSilentPush
1305
+ ? `pod 'CapacitorPluginSilentNotifications', :path => '../../node_modules/capacitor-plugin-silent-notifications'`
1306
+ : ""}
1307
+ ${hasBackgroundFetch
1308
+ ? `pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../node_modules/@transistorsoft/capacitor-background-fetch'`
1309
+ : ""}
1110
1310
  end
1111
1311
 
1112
1312
  target 'App' do
@@ -1167,6 +1367,11 @@ function modifyXcodeProjectFile(buildDir, appVersion, iosCfg) {
1167
1367
  fileContent = fileContent.replaceAll(/MARKETING_VERSION = 1.0;/g, `MARKETING_VERSION = ${appVersion};`);
1168
1368
  (0, fs_extra_1.writeFileSync)(projectFile, fileContent, "utf8");
1169
1369
  }
1370
+ /**
1371
+ * Convert a semver string to an Android integer version code (`major*1e6 + minor*1e3 + patch`).
1372
+ * @param appVersion semver string e.g. `"1.2.3"`
1373
+ * @returns integer version code
1374
+ */
1170
1375
  function generateAndroidVersionCode(appVersion) {
1171
1376
  if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(appVersion) || appVersion === "0.0.0")
1172
1377
  throw new Error(`Invalid app version '${appVersion}'`);
@@ -1175,6 +1380,12 @@ function generateAndroidVersionCode(appVersion) {
1175
1380
  parseInt(parts[1]) * 1000 +
1176
1381
  parseInt(parts[2]));
1177
1382
  }
1383
+ /**
1384
+ * Patch `build.gradle` with the app version and optional keystore signing config.
1385
+ * @param buildDir directory where the app will be built
1386
+ * @param appVersion semver string used for `versionName` and `versionCode`
1387
+ * @param keyStoreData signing credentials; omit for unsigned debug builds
1388
+ */
1178
1389
  function modifyGradleConfig(buildDir, appVersion, keyStoreData) {
1179
1390
  console.log("modifyGradleConfig");
1180
1391
  const gradleFile = (0, path_1.join)(buildDir, "android", "app", "build.gradle");