@saltcorn/mobile-builder 1.7.0-alpha.0 → 1.7.0-beta.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.
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.prepareBuildDirBase = prepareBuildDirBase;
7
+ exports.installCapacitorDeps = installCapacitorDeps;
6
8
  exports.prepareBuildDir = prepareBuildDir;
7
9
  exports.writeCapacitorConfig = writeCapacitorConfig;
8
10
  exports.prepAppIcon = prepAppIcon;
@@ -23,12 +25,15 @@ exports.writeEntitlementsPlist = writeEntitlementsPlist;
23
25
  exports.injectCodeSignEntitlements = injectCodeSignEntitlements;
24
26
  exports.copyShareExtFiles = copyShareExtFiles;
25
27
  exports.modifyShareViewController = modifyShareViewController;
28
+ exports.createShareExtensionTarget = createShareExtensionTarget;
26
29
  exports.modifyAppDelegate = modifyAppDelegate;
27
30
  exports.writePrivacyInfo = writePrivacyInfo;
28
31
  exports.copyServerFiles = copyServerFiles;
29
32
  exports.copySiteLogo = copySiteLogo;
30
33
  exports.writeCfgFile = writeCfgFile;
31
34
  exports.buildTablesFile = buildTablesFile;
35
+ exports.zipDirectory = zipDirectory;
36
+ exports.fetchRemoteAppBundle = fetchRemoteAppBundle;
32
37
  exports.copyTranslationFiles = copyTranslationFiles;
33
38
  exports.createSqliteDb = createSqliteDb;
34
39
  exports.prepareSplashPage = prepareSplashPage;
@@ -46,39 +51,52 @@ const page_1 = __importDefault(require("@saltcorn/data/models/page"));
46
51
  const file_1 = __importDefault(require("@saltcorn/data/models/file"));
47
52
  const state_1 = require("@saltcorn/data/db/state");
48
53
  const xml2js_1 = require("xml2js");
54
+ const zip_1 = require("@saltcorn/admin-models/models/zip");
55
+ const zip_extract_1 = require("@saltcorn/admin-models/models/zip-extract");
49
56
  const resizer = require("resize-with-sharp-or-jimp");
50
57
  /**
51
- * copy saltcorn-mobile-app as a template to buildDir
52
- * and install the capacitor and cordova modules to node_modules (cap sync will be run later)
58
+ * Copies the template into buildDir and installs base build tooling (no Capacitor deps).
53
59
  * @param buildDir directory where the app will be build
54
- * @param templateDir directory of the template code that will be copied to 'buildDir'
55
- * @param pushEnabled are push notifications enabled?
56
- * @param backgroundFetchEnabled is background fetch enabled?
57
- * @param pushSyncEnabled is push sync enabled?
60
+ * @param templateDir directory of the template code to copy
58
61
  */
59
- function prepareBuildDir(buildDir, templateDir, pushEnabled, backgroundFetchEnabled, pushSyncEnabled) {
60
- const state = (0, state_1.getState)();
61
- if (!state)
62
- throw new Error("Unable to get the state object");
62
+ function prepareBuildDirBase(buildDir, templateDir) {
63
63
  if ((0, fs_extra_1.existsSync)(buildDir))
64
64
  (0, fs_extra_1.rmSync)(buildDir, { force: true, recursive: true });
65
65
  (0, fs_extra_1.copySync)(templateDir, buildDir);
66
66
  (0, fs_extra_1.rmSync)(`${buildDir}/node_modules`, { recursive: true, force: true });
67
- let result = (0, child_process_1.spawnSync)("npm", ["install"], {
67
+ const result = (0, child_process_1.spawnSync)("npm", ["install"], {
68
68
  cwd: buildDir,
69
69
  });
70
70
  console.log(result.output.toString());
71
+ }
72
+ /**
73
+ * Installs Capacitor/native/Cordova deps - build-machine only, not the remote server.
74
+ * @param buildDir prepareBuildDirBase'd build directory
75
+ * @param pushEnabled include push-notification native deps
76
+ * @param backgroundFetchEnabled include background-fetch native deps
77
+ * @param pushSyncEnabled include push-sync native deps
78
+ * @param includeNativeBuildTools install CLI/asset/Cordova tools too - false for a remote bundle
79
+ */
80
+ function installCapacitorDeps(buildDir, pushEnabled, backgroundFetchEnabled, pushSyncEnabled, includeNativeBuildTools = true) {
81
+ const state = (0, state_1.getState)();
82
+ if (!state)
83
+ throw new Error("Unable to get the state object");
71
84
  // cap-plugins needed for saltcorn-plugins
72
85
  const additionalPlugins = state.capacitorPlugins.map((plugin) => `${plugin.name}@${plugin.version}`);
73
86
  console.log("installing capacitor deps and plugins");
74
87
  const capDepsAndPlugins = [
75
- "@capacitor/cli@7.4.5",
88
+ ...(includeNativeBuildTools
89
+ ? [
90
+ "@capacitor/cli@7.4.5",
91
+ "@capacitor/assets@3.0.5",
92
+ "@capacitor-community/sqlite@7.0.3",
93
+ ]
94
+ : []),
76
95
  "@capacitor/core@7.4.5",
77
- "@capacitor/assets@3.0.5",
78
96
  "@capacitor/filesystem@7.1.6",
97
+ "@capacitor/file-transfer@1.0.12",
79
98
  "@capacitor/camera@7.0.3",
80
99
  "@capacitor/network@7.0.3",
81
- "@capacitor-community/sqlite@7.0.3",
82
100
  "@capacitor/screen-orientation@7.0.3",
83
101
  "@capacitor/app@7.1.0",
84
102
  "send-intent@7.0.0",
@@ -92,20 +110,32 @@ function prepareBuildDir(buildDir, templateDir, pushEnabled, backgroundFetchEnab
92
110
  ...(pushSyncEnabled ? ["capacitor-plugin-silent-notifications@7.0.1"] : []),
93
111
  ];
94
112
  console.log("capDepsAndPlugins", capDepsAndPlugins);
95
- result = (0, child_process_1.spawnSync)("npm", ["install", "--legacy-peer-deps", ...capDepsAndPlugins], {
113
+ let result = (0, child_process_1.spawnSync)("npm", ["install", "--legacy-peer-deps", ...capDepsAndPlugins], {
96
114
  cwd: buildDir,
97
115
  maxBuffer: 1024 * 1024 * 10,
98
116
  });
99
117
  console.log(result.output.toString());
100
- console.log("installing cordova plugins");
101
- const cordovaPlugins = [
102
- "cordova-plugin-file@8.1.3",
103
- "cordova-plugin-inappbrowser@6.0.0",
104
- ];
105
- result = (0, child_process_1.spawnSync)("npm", ["install", "--legacy-peer-deps", ...cordovaPlugins], {
106
- cwd: buildDir,
107
- maxBuffer: 1024 * 1024 * 10,
108
- });
118
+ if (includeNativeBuildTools) {
119
+ console.log("installing cordova plugins");
120
+ const cordovaPlugins = [
121
+ "cordova-plugin-file@8.1.3",
122
+ "cordova-plugin-inappbrowser@6.0.0",
123
+ ];
124
+ result = (0, child_process_1.spawnSync)("npm", ["install", "--legacy-peer-deps", ...cordovaPlugins], {
125
+ cwd: buildDir,
126
+ maxBuffer: 1024 * 1024 * 10,
127
+ });
128
+ }
129
+ }
130
+ /**
131
+ * Full local build dir setup: template + build tooling + Capacitor/native
132
+ * deps. Used by the normal (non-remote) build path, which does everything
133
+ * on one machine.
134
+ * @param includeNativeBuildTools forwarded to installCapacitorDeps - false for a remote bundle
135
+ */
136
+ function prepareBuildDir(buildDir, templateDir, pushEnabled, backgroundFetchEnabled, pushSyncEnabled, includeNativeBuildTools = true) {
137
+ prepareBuildDirBase(buildDir, templateDir);
138
+ installCapacitorDeps(buildDir, pushEnabled, backgroundFetchEnabled, pushSyncEnabled, includeNativeBuildTools);
109
139
  }
110
140
  /**
111
141
  * Write `capacitor.config.ts` into the build directory.
@@ -121,9 +151,8 @@ const config: CapacitorConfig = {
121
151
  appId: '${config.appId ? config.appId : "com.saltcorn.mobile.app"}',
122
152
  appName: '${config.appName ? config.appName : "SaltcornMobileApp"}',
123
153
  webDir: "www",
124
- ios: {
125
- scheme: "SaltcornMobileApp",
126
- },
154
+ // ios.limitsNavigationsToAppBoundDomains was dropped - it caused noisy
155
+ // script-injection warnings and wasn't needed for the cookie fix to work.
127
156
  android: {
128
157
  buildOptions: {
129
158
  ${config.keystorePath && config.buildType === "release"
@@ -135,15 +164,21 @@ const config: CapacitorConfig = {
135
164
  : ""}
136
165
  releaseType: '${config.buildType === "release" ? "AAB" : "APK"}',
137
166
  },
138
- ${config.unsecureNetwork ? "allowMixedContent: true," : ""}
139
167
  },
140
- ${config.unsecureNetwork
141
- ? `server: {
142
- cleartext: true,
143
- androidScheme: 'http',
144
- },`
145
- : ""}
168
+ server: {
169
+ // App's origin on iOS - must match MOBILE_APP_ORIGINS in server/app.js.
170
+ iosScheme: 'SaltcornMobileApp',
171
+ },
146
172
  plugins: {
173
+ // Needed together with WKAppBoundDomains (see modifyInfoPlist) to
174
+ // exempt the remote server's session cookie from iOS's third-party
175
+ // cookie blocking.
176
+ CapacitorHttp: {
177
+ enabled: true,
178
+ },
179
+ CapacitorCookies: {
180
+ enabled: true,
181
+ },
147
182
  CapacitorSQLite: {
148
183
  iosDatabaseLocation: 'Library/CapacitorDatabase',
149
184
  iosIsEncryption: true,
@@ -236,9 +271,8 @@ function androidFeatures() {
236
271
  * @param allowShareTo add share-to intent filter
237
272
  * @param allowFCM add FCM push notification support
238
273
  * @param allowAuthIntent add authentication intent
239
- * @param allowClearTextTraffic allow HTTP cleartext traffic
240
274
  */
241
- async function modifyAndroidManifest(buildDir, allowShareTo, allowFCM, allowAuthIntent, allowClearTextTraffic) {
275
+ async function modifyAndroidManifest(buildDir, allowShareTo, allowFCM, allowAuthIntent) {
242
276
  console.log("modifyAndroidManifest");
243
277
  try {
244
278
  const androidManifest = (0, path_1.join)(buildDir, "android", "app", "src", "main", "AndroidManifest.xml");
@@ -256,9 +290,6 @@ async function modifyAndroidManifest(buildDir, allowShareTo, allowFCM, allowAuth
256
290
  "android:fullBackupContent": "false",
257
291
  "android:dataExtractionRules": "@xml/data_extraction_rules",
258
292
  "android:networkSecurityConfig": "@xml/network_security_config",
259
- ...(allowClearTextTraffic
260
- ? { "android:usesCleartextTraffic": "true" }
261
- : {}),
262
293
  };
263
294
  if (allowFCM) {
264
295
  parsed.manifest.application[0]["meta-data"] = [
@@ -399,24 +430,14 @@ function extractDomain(url) {
399
430
  return domain;
400
431
  }
401
432
  /**
402
- * Write the Android network security config.
403
- * When `allowClearTextTraffic` is true, permits HTTP for the server domain.
404
- * Otherwise writes a restrictive config (HTTPS only).
433
+ * Write the Android network security config. HTTPS is required, so this is
434
+ * always the restrictive (no cleartext) version.
405
435
  * @param buildDir directory where the app will be built
406
- * @param serverPath server URL used to extract the allowed domain
407
- * @param allowClearTextTraffic permit HTTP cleartext traffic to the server domain
408
436
  */
409
- function writeNetworkSecurityConfig(buildDir, serverPath, allowClearTextTraffic) {
437
+ function writeNetworkSecurityConfig(buildDir) {
410
438
  console.log("writeNetworkSecurityConfig");
411
439
  const networkSecurityConfig = (0, path_1.join)(buildDir, "android", "app", "src", "main", "res", "xml", "network_security_config.xml");
412
- const content = allowClearTextTraffic
413
- ? `<?xml version="1.0" encoding="utf-8"?>
414
- <network-security-config>
415
- <domain-config cleartextTrafficPermitted="true">
416
- <domain includeSubdomains="true">${extractDomain(serverPath)}</domain>
417
- </domain-config>
418
- </network-security-config>`
419
- : `<?xml version="1.0" encoding="utf-8"?>
440
+ const content = `<?xml version="1.0" encoding="utf-8"?>
420
441
  <network-security-config>
421
442
  <base-config cleartextTrafficPermitted="false" />
422
443
  </network-security-config>`;
@@ -548,12 +569,13 @@ function prepareExportOptionsPlist({ buildDir, appId, iosParams }) {
548
569
  <string>${iosParams.shareExtensionProvisioningProfile.guuid}</string>`;
549
570
  };
550
571
  try {
572
+ const method = iosParams.isAdHoc ? "ad-hoc" : "app-store-connect";
551
573
  (0, fs_extra_1.writeFileSync)((0, path_1.join)(buildDir, "ExportOptions.plist"), `<?xml version="1.0" encoding="UTF-8"?>
552
574
  <!DOCTYPE plist PUBLIC "~//Apple/DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
553
575
  <plist version="1.0">
554
576
  <dict>
555
577
  <key>method</key>
556
- <string>app-store-connect</string>
578
+ <string>${method}</string>
557
579
  <key>provisioningProfiles</key>
558
580
  <dict>
559
581
  <key>${appId}</key>
@@ -574,12 +596,22 @@ function prepareExportOptionsPlist({ buildDir, appId, iosParams }) {
574
596
  * @param allowShareTo add URL scheme for share-to integration
575
597
  * @param backgroundSyncEnabled background sync task is scheduled
576
598
  * @param pushSyncEnabled add remote-notification background mode
577
- * @param allowClearTextTraffic allow arbitrary HTTP loads
578
599
  * @param backgroundFetchEnabled background.js is included (sync or heartbeat configured)
600
+ * @param serverURL server URL, added to WKAppBoundDomains for the iOS cookie
601
+ * fix - changing servers (e.g. a new ngrok URL) needs a rebuild to apply.
579
602
  */
580
- function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSyncEnabled, allowClearTextTraffic, backgroundFetchEnabled = false) {
603
+ function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSyncEnabled, backgroundFetchEnabled = false, serverURL) {
581
604
  const infoPlist = (0, path_1.join)(buildDir, "ios", "App", "App", "Info.plist");
582
605
  const content = (0, fs_extra_1.readFileSync)(infoPlist, "utf8");
606
+ let appBoundHostname;
607
+ if (serverURL) {
608
+ try {
609
+ appBoundHostname = new URL(serverURL).hostname;
610
+ }
611
+ catch {
612
+ // invalid/missing URL - skip WKAppBoundDomains rather than write a broken plist
613
+ }
614
+ }
583
615
  const backgroundModes = [
584
616
  ...(backgroundFetchEnabled ? ["fetch", "processing"] : []),
585
617
  ...(pushSyncEnabled ? ["remote-notification"] : []),
@@ -587,7 +619,9 @@ function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSync
587
619
  const taskIds = [
588
620
  "com.transistorsoft.fetch",
589
621
  ...(backgroundSyncEnabled ? ["com.transistorsoft.background_sync"] : []),
590
- ...(backgroundFetchEnabled ? ["com.transistorsoft.push_sync_heartbeat"] : []),
622
+ ...(backgroundFetchEnabled
623
+ ? ["com.transistorsoft.push_sync_heartbeat"]
624
+ : []),
591
625
  ];
592
626
  const newCfgs = `
593
627
  ${backgroundFetchEnabled
@@ -619,6 +653,12 @@ function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSync
619
653
  <true/>
620
654
  <key>LSSupportsOpeningDocumentsInPlace</key>
621
655
  <true/>
656
+ ${appBoundHostname
657
+ ? `<key>WKAppBoundDomains</key>
658
+ <array>
659
+ <string>${appBoundHostname}</string>
660
+ </array>`
661
+ : ""}
622
662
  ${allowShareTo
623
663
  ? `<key>CFBundleURLTypes</key>
624
664
  <array>
@@ -634,27 +674,25 @@ function modifyInfoPlist(buildDir, allowShareTo, backgroundSyncEnabled, pushSync
634
674
  </dict>
635
675
  </array>`
636
676
  : ""}
637
- ${allowClearTextTraffic
638
- ? `<key>NSAppTransportSecurity</key>
639
- <dict>
640
- <key>NSAllowsArbitraryLoads</key>
641
- <true/>
642
- </dict>`
643
- : ""}
644
677
  `;
645
678
  // add newCfgs after the first <dict> tag
646
679
  const newContent = content.replace(/<dict>/, `<dict>${newCfgs}`);
647
680
  (0, fs_extra_1.writeFileSync)(infoPlist, newContent, "utf8");
648
681
  }
649
682
  /**
650
- * Write `App.entitlements` with the APNs environment and optional app group.
683
+ * Write `App.entitlements` with the optional APNs environment and app group.
651
684
  * @param buildDir directory where the app will be built
652
685
  * @param buildType `"debug"` → development APNs, `"release"` → production APNs
686
+ * @param pushSync include the APNs environment entitlement
653
687
  * @param appGroupId optional app group ID for share extension data sharing
654
688
  */
655
- function writeEntitlementsPlist(buildDir, buildType, appGroupId) {
689
+ function writeEntitlementsPlist(buildDir, buildType, pushSync, appGroupId) {
656
690
  const file = (0, path_1.join)(buildDir, "ios", "App", "App", "App.entitlements");
657
691
  const apsEnv = buildType === "release" ? "production" : "development";
692
+ const apsEnvEntry = pushSync
693
+ ? ` <key>aps-environment</key>
694
+ <string>${apsEnv}</string>`
695
+ : "";
658
696
  const appGroupsEntry = appGroupId
659
697
  ? ` <key>com.apple.security.application-groups</key>
660
698
  <array>
@@ -666,8 +704,7 @@ function writeEntitlementsPlist(buildDir, buildType, appGroupId) {
666
704
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
667
705
  <plist version="1.0">
668
706
  <dict>
669
- <key>aps-environment</key>
670
- <string>${apsEnv}</string>
707
+ ${apsEnvEntry}
671
708
  ${appGroupsEntry}
672
709
  </dict>
673
710
  </plist>`);
@@ -686,7 +723,9 @@ function injectCodeSignEntitlements(buildDir) {
686
723
  const pbxprojPath = (0, path_1.join)(buildDir, "ios", "App", "App.xcodeproj", "project.pbxproj");
687
724
  try {
688
725
  let content = (0, fs_extra_1.readFileSync)(pbxprojPath, "utf8");
689
- if (content.includes("CODE_SIGN_ENTITLEMENTS")) {
726
+ // check for this exact insertion, not just any CODE_SIGN_ENTITLEMENTS -
727
+ // the share-ext target sets its own, which isn't the same thing
728
+ if (content.includes("CODE_SIGN_ENTITLEMENTS = App/App.entitlements;")) {
690
729
  console.log("CODE_SIGN_ENTITLEMENTS already set in project.pbxproj");
691
730
  return;
692
731
  }
@@ -720,6 +759,52 @@ function modifyShareViewController(buildDir, groupId) {
720
759
  content = content.replace(/YOUR_APP_GROUP_ID/g, groupId);
721
760
  (0, fs_extra_1.writeFileSync)(shareVCFile, content, "utf8");
722
761
  }
762
+ /**
763
+ * Write the entitlements plist for the share extension target (app group
764
+ * only - no APNs entry, extensions don't receive push notifications).
765
+ * @param buildDir directory where the app will be built
766
+ * @param appGroupId app group ID shared with the main app
767
+ * @returns path to the entitlements file, relative to `ios/App`
768
+ */
769
+ function writeShareExtEntitlementsPlist(buildDir, appGroupId) {
770
+ const relPath = (0, path_1.join)("share-ext", "ShareExtension.entitlements");
771
+ const file = (0, path_1.join)(buildDir, "ios", "App", relPath);
772
+ (0, fs_extra_1.writeFileSync)(file, `<?xml version="1.0" encoding="UTF-8"?>
773
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
774
+ <plist version="1.0">
775
+ <dict>
776
+ <key>com.apple.security.application-groups</key>
777
+ <array>
778
+ <string>${appGroupId}</string>
779
+ </array>
780
+ </dict>
781
+ </plist>`);
782
+ return relPath;
783
+ }
784
+ /**
785
+ * Add a "share-ext" Share Extension target to `App.xcodeproj`, so the app
786
+ * can be built with iOS share-to support without ever opening Xcode.
787
+ * @param buildDir directory where the app will be built
788
+ * @param appId main app's bundle id - the extension's is `${appId}.share-ext`
789
+ * @param appGroupId optional app group ID, wired into a matching
790
+ * entitlements plist for the new target
791
+ */
792
+ function createShareExtensionTarget(buildDir, appId, appGroupId) {
793
+ const iosAppDir = (0, path_1.join)(buildDir, "ios", "App");
794
+ const projectPath = (0, path_1.join)(iosAppDir, "App.xcodeproj");
795
+ const entitlementsRelPath = appGroupId
796
+ ? writeShareExtEntitlementsPlist(buildDir, appGroupId)
797
+ : undefined;
798
+ const scriptPath = (0, path_1.join)(__dirname, "xcode", "create_share_extension.rb");
799
+ const args = [scriptPath, projectPath, `${appId}.share-ext`];
800
+ if (entitlementsRelPath)
801
+ args.push(entitlementsRelPath);
802
+ const result = (0, child_process_1.spawnSync)("ruby", args, { cwd: iosAppDir });
803
+ if (result.status !== 0) {
804
+ throw new Error(`Unable to create the share extension target: ${result.stderr?.toString() || result.error?.message || "unknown error"}`);
805
+ }
806
+ console.log(result.stdout?.toString());
807
+ }
723
808
  /**
724
809
  * Patch `AppDelegate.swift` to register background tasks and push handlers.
725
810
  * @param buildDir directory where the app will be built
@@ -734,7 +819,7 @@ function modifyAppDelegate(buildDir, pushSyncEnabled, allowShareTo, backgroundFe
734
819
  content = content.replace(/func application\(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: \[UIApplication.LaunchOptionsKey: Any\]\?\) -> Bool {/, `func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
735
820
  // Override point for customization after application launch.
736
821
 
737
-
822
+
738
823
  ${backgroundFetchEnabled
739
824
  ? `// [capacitor-background-fetch]
740
825
  let fetchManager = TSBackgroundFetch.sharedInstance();
@@ -989,91 +1074,86 @@ function writeCfgFile({ buildDir, entryPoint, entryPointType, serverPath, synche
989
1074
  cfg.tenantAppName = tenantAppName;
990
1075
  (0, fs_extra_1.writeFileSync)((0, path_1.join)(buildDir, "www", "data", "config.js"), `var _sc_mobile_config = ${JSON.stringify(cfg)}`);
991
1076
  }
992
- /**
993
- * create a file with all data from the db
994
- * the app updates its local db from this
995
- * @param buildDir directory where the app will be build
996
- * @param includedPlugins names of plugins that are bundled into the app
997
- */
998
- async function buildTablesFile(buildDir, includedPlugins) {
999
- const state = (0, state_1.getState)();
1000
- if (!state)
1001
- throw new Error("Unable to get the state object");
1002
- await state.refresh_config(true);
1003
- // remove cfgs with excludeFromMobile or input_type=password
1004
- const filterPluginFunc = async (plugin) => {
1005
- let module = state.plugins[plugin.name];
1006
- if (!module)
1007
- module = state.plugins[state.plugin_module_names[plugin.name]];
1008
- if (module?.configuration_workflow) {
1077
+ const filterPluginFunc = async (state, plugin) => {
1078
+ let module = state.plugins[plugin.name];
1079
+ if (!module)
1080
+ module = state.plugins[state.plugin_module_names[plugin.name]];
1081
+ if (module?.configuration_workflow) {
1082
+ try {
1083
+ const flow = await module.configuration_workflow();
1084
+ for (const step of flow?.steps || []) {
1085
+ if (step.form) {
1086
+ const form = await step.form({});
1087
+ for (const field of form?.fields || []) {
1088
+ if (field.exclude_from_mobile || field.input_type === "password") {
1089
+ delete plugin.configuration[field.name];
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ catch (error) {
1096
+ console.log(`Error in configuration_workflow of plugin ${plugin.name}`);
1097
+ console.log(error);
1098
+ }
1099
+ }
1100
+ return plugin;
1101
+ };
1102
+ const filterTableFunc = async (state, table) => {
1103
+ let result = table;
1104
+ if (table.provider_name) {
1105
+ const oldProviderCfg = JSON.parse(JSON.stringify(table.provider_cfg || {}));
1106
+ const provider = state.table_providers[table.provider_name];
1107
+ if (provider?.configuration_workflow) {
1009
1108
  try {
1010
- const flow = await module.configuration_workflow();
1109
+ const flow = await provider.configuration_workflow();
1011
1110
  for (const step of flow?.steps || []) {
1012
1111
  if (step.form) {
1013
- const form = await step.form({});
1112
+ const form = await step.form(oldProviderCfg);
1014
1113
  for (const field of form?.fields || []) {
1015
1114
  if (field.exclude_from_mobile ||
1016
1115
  field.input_type === "password") {
1017
- delete plugin.configuration[field.name];
1116
+ delete result.provider_cfg[field.name];
1018
1117
  }
1019
1118
  }
1020
1119
  }
1021
1120
  }
1022
1121
  }
1023
1122
  catch (error) {
1024
- console.log(`Error in configuration_workflow of plugin ${plugin.name}`);
1123
+ console.log(`Error in configuration_workflow of table provider ${table.provider_name}`);
1025
1124
  console.log(error);
1026
1125
  }
1027
1126
  }
1028
- return plugin;
1029
- };
1030
- const filterTableFunc = async (table) => {
1031
- let result = table;
1032
- if (table.provider_name) {
1033
- const oldProviderCfg = JSON.parse(JSON.stringify(table.provider_cfg || {}));
1034
- const provider = state.table_providers[table.provider_name];
1035
- if (provider?.configuration_workflow) {
1036
- try {
1037
- const flow = await provider.configuration_workflow();
1038
- for (const step of flow?.steps || []) {
1039
- if (step.form) {
1040
- const form = await step.form(oldProviderCfg);
1041
- for (const field of form?.fields || []) {
1042
- if (field.exclude_from_mobile ||
1043
- field.input_type === "password") {
1044
- delete result.provider_cfg[field.name];
1045
- }
1046
- }
1047
- }
1048
- }
1049
- }
1050
- catch (error) {
1051
- console.log(`Error in configuration_workflow of table provider ${table.provider_name}`);
1052
- console.log(error);
1053
- }
1054
- }
1055
- }
1056
- return result;
1057
- };
1058
- const filterFunc = async (table, rows) => {
1059
- switch (table) {
1060
- case "_sc_plugins":
1061
- const included = rows.filter((plugin) => includedPlugins ? includedPlugins.includes(plugin.name) : true);
1062
- return await Promise.all(included.map(filterPluginFunc));
1063
- case "_sc_config":
1064
- const allCfgs = state.configs;
1065
- // remove cfgs with excludeFromMobile or input_type=password
1066
- return rows.filter((row) => {
1067
- const cfg = allCfgs[row.key];
1068
- return (cfg && !(cfg.excludeFromMobile || cfg.input_type === "password"));
1069
- });
1070
- case "_sc_tables":
1071
- return await Promise.all(rows.map(filterTableFunc));
1072
- default:
1073
- return rows;
1074
- }
1075
- };
1076
- const wwwDir = (0, path_1.join)(buildDir, "www", "data");
1127
+ }
1128
+ return result;
1129
+ };
1130
+ const filterFunc = async (state, table, rows, includedPlugins) => {
1131
+ switch (table) {
1132
+ case "_sc_plugins":
1133
+ const included = rows.filter((plugin) => includedPlugins ? includedPlugins.includes(plugin.name) : true);
1134
+ return await Promise.all(included.map((plugin) => filterPluginFunc(state, plugin)));
1135
+ case "_sc_config":
1136
+ const allCfgs = state.configs;
1137
+ // remove cfgs with excludeFromMobile or input_type=password
1138
+ return rows.filter((row) => {
1139
+ const cfg = allCfgs[row.key];
1140
+ return (cfg && !(cfg.excludeFromMobile || cfg.input_type === "password"));
1141
+ });
1142
+ case "_sc_tables":
1143
+ return await Promise.all(rows.map((table) => filterTableFunc(state, table)));
1144
+ default:
1145
+ return rows;
1146
+ }
1147
+ };
1148
+ /**
1149
+ * Reads and filters all _sc_ table rows for the sync/tables data dump.
1150
+ * @param includedPlugins names of plugins that are bundled into the app
1151
+ */
1152
+ const computeTablesData = async (includedPlugins) => {
1153
+ const state = (0, state_1.getState)();
1154
+ if (!state)
1155
+ throw new Error("Unable to get the state object");
1156
+ await state.refresh_config(true);
1077
1157
  const scTables = (await index_1.default.listScTables()).filter((table) => [
1078
1158
  "_sc_migrations",
1079
1159
  "_sc_errors",
@@ -1087,18 +1167,141 @@ async function buildTablesFile(buildDir, includedPlugins) {
1087
1167
  const dbData = await index_1.default.select(row.name);
1088
1168
  return {
1089
1169
  table: row.name,
1090
- rows: await filterFunc(row.name, dbData),
1170
+ rows: await filterFunc(state, row.name, dbData, includedPlugins),
1091
1171
  };
1092
1172
  }));
1093
- const createdAt = new Date();
1173
+ return { createdAt: new Date(), sc_tables: tablesWithData };
1174
+ };
1175
+ /**
1176
+ * create a file with all data from the db
1177
+ * the app updates its local db from this
1178
+ * @param buildDir directory where the app will be build
1179
+ * @param includedPlugins names of plugins that are bundled into the app
1180
+ */
1181
+ async function buildTablesFile(buildDir, includedPlugins) {
1182
+ const wwwDir = (0, path_1.join)(buildDir, "www", "data");
1183
+ const { createdAt, sc_tables } = await computeTablesData(includedPlugins);
1094
1184
  (0, fs_extra_1.writeFileSync)((0, path_1.join)(wwwDir, "tables.json"), JSON.stringify({
1095
1185
  created_at: createdAt.valueOf(),
1096
- sc_tables: tablesWithData,
1186
+ sc_tables,
1097
1187
  }));
1098
1188
  (0, fs_extra_1.writeFileSync)((0, path_1.join)(wwwDir, "tables_created_at.json"), JSON.stringify({
1099
1189
  created_at: createdAt.valueOf(),
1100
1190
  }));
1101
1191
  }
1192
+ const walkDir = (dir) => {
1193
+ const results = [];
1194
+ for (const entry of (0, fs_extra_1.readdirSync)(dir, { withFileTypes: true })) {
1195
+ const full = (0, path_1.join)(dir, entry.name);
1196
+ if (entry.isDirectory())
1197
+ results.push(...walkDir(full));
1198
+ else
1199
+ results.push(full);
1200
+ }
1201
+ return results;
1202
+ };
1203
+ /**
1204
+ * Zip a directory's contents (recursively), entry names relative to sourceDir.
1205
+ * @param sourceDir directory to zip
1206
+ * @param outputPath path of the zip file to write
1207
+ */
1208
+ async function zipDirectory(sourceDir, outputPath) {
1209
+ const zip = await zip_1.ZipBuilder.toFile(outputPath);
1210
+ for (const filePath of walkDir(sourceDir)) {
1211
+ const entryName = (0, path_1.relative)(sourceDir, filePath).split(path_1.sep).join("/");
1212
+ await zip.addLocalFile(entryName, filePath);
1213
+ }
1214
+ await zip.finish();
1215
+ }
1216
+ /**
1217
+ * Fetches the remote server's prepared www/ bundle and extracts it into buildDir/www.
1218
+ * @param buildDir directory where the app will be build
1219
+ * @param remoteSchemaUrl the /api/mobile-app/build-bundle URL
1220
+ * @param apiKey bearer token for an admin user on that server
1221
+ * @param options this machine's build settings, so the remote content matches
1222
+ */
1223
+ async function fetchRemoteAppBundle(buildDir, remoteSchemaUrl, apiKey, options) {
1224
+ const url = new URL(`${remoteSchemaUrl.replace(/\/+$/, "")}/api/mobile-app/build-bundle`);
1225
+ if (options.includedPlugins?.length)
1226
+ url.searchParams.set("includedPlugins", options.includedPlugins.join(","));
1227
+ for (const platform of options.platforms)
1228
+ url.searchParams.append("platforms", platform);
1229
+ for (const table of options.synchedTables || [])
1230
+ url.searchParams.append("synchedTables", table);
1231
+ // empty in by-role mode - omit rather than send the literal string "undefined"
1232
+ if (options.entryPoint)
1233
+ url.searchParams.set("entryPoint", options.entryPoint);
1234
+ url.searchParams.set("entryPointType", options.entryPointType);
1235
+ url.searchParams.set("serverURL", options.serverURL);
1236
+ if (options.splashPage)
1237
+ url.searchParams.set("splashPage", options.splashPage);
1238
+ if (options.tenantAppName)
1239
+ url.searchParams.set("tenantAppName", options.tenantAppName);
1240
+ url.searchParams.set("buildType", options.buildType);
1241
+ if (options.autoPublicLogin)
1242
+ url.searchParams.set("autoPublicLogin", options.autoPublicLogin);
1243
+ if (options.showContinueAsPublicUser)
1244
+ url.searchParams.set("showContinueAsPublicUser", "true");
1245
+ if (options.allowOfflineMode)
1246
+ url.searchParams.set("allowOfflineMode", options.allowOfflineMode);
1247
+ if (options.syncOnReconnect)
1248
+ url.searchParams.set("syncOnReconnect", "true");
1249
+ if (options.syncOnAppResume)
1250
+ url.searchParams.set("syncOnAppResume", "true");
1251
+ if (options.pushSync)
1252
+ url.searchParams.set("pushSync", "true");
1253
+ if (options.syncInterval)
1254
+ url.searchParams.set("syncInterval", String(options.syncInterval));
1255
+ if (options.pushSyncHeartbeatInterval)
1256
+ url.searchParams.set("pushSyncHeartbeatInterval", String(options.pushSyncHeartbeatInterval));
1257
+ const headers = apiKey
1258
+ ? { Authorization: `Bearer ${apiKey}` }
1259
+ : {};
1260
+ const readError = async (res) => {
1261
+ try {
1262
+ return (await res.json())?.error;
1263
+ }
1264
+ catch {
1265
+ return undefined;
1266
+ }
1267
+ };
1268
+ console.log(`Fetching remote app bundle from ${remoteSchemaUrl}...`);
1269
+ // poll instead of one long request - some NAT/routers kill an idle connection
1270
+ const startRes = await fetch(url.toString(), { method: "POST", headers });
1271
+ if (!startRes.ok)
1272
+ throw new Error(`Unable to start remote app bundle build at ${url}: ${(await readError(startRes)) || `${startRes.status} ${startRes.statusText}`}`);
1273
+ const { job_id } = (await startRes.json());
1274
+ // origin+pathname, not url.toString() - that still has the build options
1275
+ const statusUrl = new URL(url.pathname + "/status", url.origin);
1276
+ statusUrl.searchParams.set("job_id", job_id);
1277
+ const POLL_DEADLINE = Date.now() + 30 * 60 * 1000;
1278
+ while (true) {
1279
+ if (Date.now() > POLL_DEADLINE)
1280
+ throw new Error(`Timed out waiting for the remote app bundle build to finish (job ${job_id})`);
1281
+ await new Promise((r) => setTimeout(r, 3000));
1282
+ const statusRes = await fetch(statusUrl.toString(), { headers });
1283
+ if (!statusRes.ok)
1284
+ throw new Error(`Unable to check remote app bundle build status at ${statusUrl}: ${statusRes.status} ${statusRes.statusText}`);
1285
+ const { status, error } = (await statusRes.json());
1286
+ if (status === "error")
1287
+ throw new Error(`Unable to build remote app bundle: ${error}`);
1288
+ if (status === "done")
1289
+ break;
1290
+ }
1291
+ const resultUrl = new URL(url.pathname + "/result", url.origin);
1292
+ resultUrl.searchParams.set("job_id", job_id);
1293
+ const resultRes = await fetch(resultUrl.toString(), { headers });
1294
+ if (!resultRes.ok)
1295
+ throw new Error(`Unable to fetch remote app bundle from ${resultUrl}: ${(await readError(resultRes)) || `${resultRes.status} ${resultRes.statusText}`}`);
1296
+ // saved in buildDir (not just os.tmpdir()) so it's easy to find and
1297
+ // inspect alongside the rest of the build output; not part of the app
1298
+ // itself, so keeping it around here is harmless
1299
+ const tmpZip = (0, path_1.join)(buildDir, "remote-bundle.zip");
1300
+ (0, fs_extra_1.writeFileSync)(tmpZip, Buffer.from(await resultRes.arrayBuffer()));
1301
+ console.log(`Remote app bundle saved to ${tmpZip}`);
1302
+ await (0, zip_extract_1.extractZip)(tmpZip, (0, path_1.join)(buildDir, "www"));
1303
+ console.log("Remote app bundle fetch finished");
1304
+ }
1102
1305
  /**
1103
1306
  * copy files form 'server/locales' into the app
1104
1307
  * @param buildDir directory where the app will be build
@@ -1231,6 +1434,7 @@ function writePodfile(buildDir, hasPush, hasBackgroundFetch, hasSilentPush) {
1231
1434
  pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
1232
1435
  pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
1233
1436
  pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
1437
+ pod 'CapacitorFileTransfer', :path => '../../node_modules/@capacitor/file-transfer'
1234
1438
  ${hasGeolocation
1235
1439
  ? `pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation'`
1236
1440
  : ""}