@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.
- package/dist/copy_static_assets.cjs +10 -0
- package/dist/copy_static_assets.cjs.map +1 -0
- package/dist/copy_static_assets.d.cts +2 -0
- package/dist/copy_static_assets.d.cts.map +1 -0
- package/dist/mobile-builder.d.ts +21 -7
- package/dist/mobile-builder.d.ts.map +1 -1
- package/dist/mobile-builder.js +128 -89
- package/dist/mobile-builder.js.map +1 -1
- package/dist/utils/common-build-utils.d.ts +72 -20
- package/dist/utils/common-build-utils.d.ts.map +1 -1
- package/dist/utils/common-build-utils.js +347 -143
- package/dist/utils/common-build-utils.js.map +1 -1
- package/dist/utils/xcode/create_share_extension.rb +98 -0
- package/package.json +10 -9
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
|
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
|
|
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
|
|
1123
|
+
console.log(`Error in configuration_workflow of table provider ${table.provider_name}`);
|
|
1025
1124
|
console.log(error);
|
|
1026
1125
|
}
|
|
1027
1126
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
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
|
|
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
|
: ""}
|