@saltcorn/mobile-app 1.1.0-beta.2 → 1.1.0-beta.21

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.
Files changed (39) hide show
  1. package/.babelrc +3 -0
  2. package/build_scripts/modify_android_manifest.js +47 -0
  3. package/build_scripts/modify_gradle_cfg.js +34 -0
  4. package/package.json +20 -11
  5. package/src/.eslintrc +21 -0
  6. package/src/helpers/api.js +43 -0
  7. package/src/helpers/auth.js +191 -0
  8. package/src/helpers/common.js +175 -0
  9. package/{www/js/utils/table_utils.js → src/helpers/db_schema.js} +18 -40
  10. package/src/helpers/file_system.js +102 -0
  11. package/{www/js/utils/global_utils.js → src/helpers/navigation.js} +175 -335
  12. package/src/helpers/offline_mode.js +645 -0
  13. package/src/index.js +20 -0
  14. package/src/init.js +424 -0
  15. package/src/routing/index.js +98 -0
  16. package/{www/js → src/routing}/mocks/request.js +5 -5
  17. package/{www/js → src/routing}/mocks/response.js +1 -1
  18. package/{www/js → src/routing}/routes/api.js +10 -15
  19. package/{www/js → src/routing}/routes/auth.js +12 -6
  20. package/{www/js → src/routing}/routes/delete.js +9 -6
  21. package/{www/js → src/routing}/routes/edit.js +9 -6
  22. package/src/routing/routes/error.js +6 -0
  23. package/{www/js → src/routing}/routes/fields.js +7 -2
  24. package/{www/js → src/routing}/routes/page.js +14 -9
  25. package/{www/js → src/routing}/routes/sync.js +9 -5
  26. package/{www/js → src/routing}/routes/view.js +16 -11
  27. package/{www/js/routes/common.js → src/routing/utils.js} +15 -13
  28. package/webpack.config.js +31 -0
  29. package/www/data/encoded_site_logo.js +1 -0
  30. package/www/index.html +23 -493
  31. package/www/js/{utils/iframe_view_utils.js → iframe_view_utils.js} +187 -273
  32. package/config.xml +0 -27
  33. package/res/icon/android/icon.png +0 -0
  34. package/res/screen/android/splash-icon.png +0 -0
  35. package/res/screen/ios/Default@2x~universal~anyany.png +0 -0
  36. package/www/js/routes/error.js +0 -5
  37. package/www/js/routes/init.js +0 -76
  38. package/www/js/utils/file_helpers.js +0 -108
  39. package/www/js/utils/offline_mode_helper.js +0 -625
package/www/index.html CHANGED
@@ -5,500 +5,30 @@
5
5
  name="viewport"
6
6
  content="width=device-width, initial-scale=1, maximum-scale=1"
7
7
  />
8
- <script src="cordova.js"></script>
9
- <script src="js/utils/global_utils.js"></script>
10
- <script src="js/utils/iframe_view_utils.js"></script>
11
- <script src="js/utils/file_helpers.js"></script>
12
- <script src="js/utils/table_utils.js"></script>
13
- <script src="js/utils/offline_mode_helper.js"></script>
14
-
15
- <script src="js/mocks/request.js"></script>
16
- <script src="js/mocks/response.js"></script>
17
- <script src="js/routes/common.js"></script>
18
- <script src="js/routes/auth.js"></script>
19
- <script src="js/routes/delete.js"></script>
20
- <script src="js/routes/edit.js"></script>
21
- <script src="js/routes/api.js"></script>
22
- <script src="js/routes/error.js"></script>
23
- <script src="js/routes/page.js"></script>
24
- <script src="js/routes/view.js"></script>
25
- <script src="js/routes/fields.js"></script>
26
- <script src="js/routes/sync.js"></script>
27
- <script src="js/routes/init.js"></script>
28
-
29
- <script src="js/mocks/response.js"></script>
30
- <script src="js/mocks/request.js"></script>
31
- <script src="npm_packages/jwt-decode.js"></script>
32
- <script src="npm_packages/universal-router.min.js"></script>
33
- <script src="npm_packages/axios.min.js"></script>
34
- <script src="npm_packages/i18next.min.js"></script>
35
- <script src="npm_packages/i18nextSprintfPostProcessor.min.js"></script>
36
- <script>
37
- const staticPlugins = ["base", "sbadmin2"];
38
-
39
- async function addScript(scriptObj) {
40
- let waited = 0;
41
- const maxWait = 3000;
42
-
43
- const moduleAvailable = () =>
44
- window.saltcorn && window.saltcorn[scriptObj.name];
45
-
46
- return new Promise((resolve, reject) => {
47
- let script = document.createElement("script");
48
- document.head.appendChild(script);
49
-
50
- const waitForModule = () => {
51
- waited += 100;
52
- if (waited >= maxWait)
53
- return reject(`unable to load '${scriptObj.name}'`);
54
- console.log("waiting for " + scriptObj.name);
55
- setTimeout(function () {
56
- if (moduleAvailable()) return resolve();
57
- else waitForModule();
58
- }, 100);
59
- };
60
-
61
- script.onload = () => {
62
- if (!scriptObj.name || moduleAvailable()) return resolve();
63
- waitForModule();
64
- };
65
- script.src = scriptObj.src;
66
- });
67
- }
68
-
69
- async function loadPlugin(plugin) {
70
- await addScript({
71
- src: `js/bundle/${plugin.name}.bundle.js`,
72
- name: plugin.name,
73
- });
74
- }
75
-
76
- async function loadPlugins(state) {
77
- const plugins = (await saltcorn.data.models.Plugin.find()).filter(
78
- (plugin) => !staticPlugins.includes(plugin.name)
79
- );
80
- for (const plugin of plugins) {
81
- await loadPlugin(plugin);
82
- state.registerPlugin(
83
- plugin.name,
84
- saltcorn[plugin.name],
85
- plugin.configuration
86
- );
87
- }
88
- return plugins;
89
- }
90
-
91
- /**
92
- * add <script> tags dynamically
93
- */
94
- async function addScripts(version_tag) {
95
- const scripts = [
96
- { src: `static_assets/${version_tag}/jquery-3.6.0.min.js` },
97
- { src: "js/bundle/common_chunks.bundle.js" },
98
- { src: "js/bundle/markup.bundle.js", name: "markup" },
99
- { src: "js/bundle/data.bundle.js", name: "data" },
100
- { src: "js/bundle/base_plugin.bundle.js", name: "base_plugin" },
101
- { src: "js/bundle/sbadmin2.bundle.js", name: "sbadmin2" },
102
- ];
103
- for (const script of scripts) {
104
- await addScript(script);
105
- }
106
- }
107
-
108
- const prepareHeader = (header) => {
109
- let result = Object.assign({}, header);
110
- if (result.script?.startsWith("/")) {
111
- result.script = result.script.substring(1);
112
- }
113
- return result;
114
- };
115
-
116
- /*
117
- A plugin exports headers either as array, as key values object, or
118
- as a function with a configuration parameter that returns an Array.
119
- */
120
- const collectPluginHeaders = (pluginRows) => {
121
- const config = saltcorn.data.state.getState().mobileConfig;
122
- config.pluginHeaders = [];
123
- for (const row of pluginRows) {
124
- const pluginHeaders = saltcorn[row.name].headers;
125
- if (pluginHeaders) {
126
- if (Array.isArray(pluginHeaders))
127
- for (const header of pluginHeaders) {
128
- config.pluginHeaders.push(prepareHeader(header));
129
- }
130
- else if (typeof pluginHeaders === "function") {
131
- const headerResult = pluginHeaders(row.configuration || {});
132
- if (Array.isArray(headerResult)) {
133
- for (const header of headerResult)
134
- config.pluginHeaders.push(prepareHeader(header));
135
- }
136
- } else
137
- for (const [key, value] of Object.entries(pluginHeaders)) {
138
- config.pluginHeaders.push(prepareHeader(value));
139
- }
140
- }
141
- }
142
- };
143
-
144
- const initJwt = async () => {
145
- if (!(await saltcorn.data.db.tableExists("jwt_table"))) {
146
- await createJwtTable();
147
- } else {
148
- const jwt = await getJwt();
149
- if (jwt) {
150
- const state = saltcorn.data.state.getState();
151
- state.mobileConfig.jwt = jwt;
152
- }
153
- }
154
- };
155
-
156
- const initI18Next = async () => {
157
- const resources = {};
158
- for (const key of Object.keys(
159
- saltcorn.data.models.config.available_languages
160
- )) {
161
- const localeFile = await readJSON(
162
- `${key}.json`,
163
- `${cordova.file.applicationDirectory}www/locales`
164
- );
165
- resources[key] = {
166
- translation: localeFile,
167
- };
168
- }
169
- await i18next.use(i18nextSprintfPostProcessor).init({
170
- lng: "en",
171
- resources,
172
- });
173
- };
174
-
175
- const readSiteLogo = async (state) => {
176
- try {
177
- const base64 = await readText(
178
- "encoded_site_logo.txt",
179
- `${cordova.file.applicationDirectory}www`
180
- );
181
- state.mobileConfig.encodedSiteLogo = base64;
182
- } catch (error) {
183
- console.log(
184
- `Unable to read the site logo file: ${
185
- error.message ? error.message : "Unknown error"
186
- }`
187
- );
188
- }
189
- };
190
-
191
- const showErrorPage = async (error) => {
192
- const state = saltcorn.data.state.getState();
193
- state.mobileConfig.inErrorState = true;
194
- const page = await router.resolve({
195
- pathname: "get/error_page",
196
- fullWrap: true,
197
- alerts: [
198
- {
199
- type: "error",
200
- msg: error.message ? error.message : "An error occured.",
201
- },
202
- ],
203
- });
204
- await replaceIframe(page.content);
205
- };
206
-
207
- // the app comes back from background
208
- const onResume = async () => {
209
- if (typeof saltcorn === "undefined") return;
210
- const state = saltcorn.data.state.getState();
211
- const mobileConfig = state.mobileConfig;
212
- if (mobileConfig?.allowOfflineMode) {
213
- mobileConfig.networkState = navigator.connection.type;
214
- if (
215
- mobileConfig.networkState === "none" &&
216
- !mobileConfig.isOfflineMode &&
217
- mobileConfig.jwt
218
- ) {
219
- try {
220
- await offlineHelper.startOfflineMode();
221
- clearHistory();
222
- if (mobileConfig.user_id) await gotoEntryView();
223
- else {
224
- const decodedJwt = jwt_decode(mobileConfig.jwt);
225
- mobileConfig.user = decodedJwt.user;
226
- // TODO remove these, use 'user' everywhere
227
- mobileConfig.role_id = decodedJwt.user.role_id
228
- ? decodedJwt.user.role_id
229
- : 100;
230
- mobileConfig.user_id = decodedJwt.user.id;
231
- mobileConfig.user_name = decodedJwt.user.email;
232
- mobileConfig.language = decodedJwt.user.language;
233
-
234
- mobileConfig.isPublicUser = false;
235
- }
236
- addRoute({ route: entryPoint, query: undefined });
237
- const page = await router.resolve({
238
- pathname: mobileConfig.entry_point,
239
- fullWrap: true,
240
- alerts: [],
241
- });
242
- } catch (error) {
243
- await showErrorPage(error);
244
- }
245
- }
246
- }
247
- };
248
-
249
- const isPublicJwt = (jwt) => {
250
- try {
251
- if (!jwt) return false;
252
- const decoded = jwt_decode(jwt);
253
- return decoded.sub === "public";
254
- } catch (error) {
255
- console.log(
256
- `Unable to inspect '${jwt}': ${
257
- error.message ? error.message : "Unknown error"
258
- }`
259
- );
260
- return false;
261
- }
262
- };
263
-
264
- const isPublicEntryPoint = async (entryPoint) => {
265
- try {
266
- const tokens = entryPoint.split("/");
267
- if (tokens.length < 3) throw new Error("The format is incorrect");
268
- const name = tokens[tokens.length - 1];
269
- const entryObj =
270
- tokens[tokens.length - 2] === "view"
271
- ? saltcorn.data.models.View.findOne({ name: name })
272
- : saltcorn.data.models.Page.findOne({ name: name });
273
- if (!entryObj) throw new Error(`The object '${name}' does not exist`);
274
- else return entryObj.min_role === 100;
275
- } catch (error) {
276
- console.log(
277
- `Unable to inspect '${entryPoint}': ${
278
- error.message ? error.message : "Unknown error"
279
- }`
280
- );
281
- return false;
282
- }
283
- };
284
-
285
- const showLogin = async (alerts) => {
286
- const page = await router.resolve({
287
- pathname: "get/auth/login",
288
- alerts,
289
- });
290
- await replaceIframe(page.content);
291
- };
292
-
293
- const takeLastLocation = () => {
294
- let result = null;
295
- const lastLocation = localStorage.getItem("lastLocation");
296
- localStorage.removeItem("lastLocation");
297
- if (lastLocation) {
298
- try {
299
- result = JSON.parse(lastLocation);
300
- } catch (error) {
301
- console.log(
302
- `Unable to parse the last location: ${
303
- error.message ? error.message : "Unknown error"
304
- }`
305
- );
306
- }
307
- }
308
- return result;
309
- };
310
-
311
- // device is ready
312
- const init = async () => {
313
- try {
314
- const lastLocation = takeLastLocation();
315
- document.addEventListener("resume", onResume, false);
316
- const config = await readJSON(
317
- "config",
318
- `${cordova.file.applicationDirectory}www`
319
- );
320
- const { created_at } = await readJSON(
321
- "tables_created_at.json",
322
- `${cordova.file.applicationDirectory}${"www"}`
323
- );
324
- let tablesJSON = null;
325
- await addScripts(config.version_tag);
326
- saltcorn.data.db.connectObj.version_tag = config.version_tag;
327
- await saltcorn.data.db.init();
328
- const updateNeeded = await dbUpdateNeeded(created_at);
329
- if (updateNeeded) {
330
- tablesJSON = await readJSON(
331
- "tables.json",
332
- `${cordova.file.applicationDirectory}${"www"}`
333
- );
334
- // update '_sc_plugins' first because of loadPlugins()
335
- await updateScPlugins(tablesJSON);
336
- }
337
- saltcorn.data.state.features.version_plugin_serve_path = false;
338
- const state = saltcorn.data.state.getState();
339
- state.mobileConfig = config;
340
- state.registerPlugin("base", saltcorn.base_plugin);
341
- state.registerPlugin("sbadmin2", saltcorn.sbadmin2);
342
- collectPluginHeaders(await loadPlugins(state));
343
- if (updateNeeded) {
344
- if (!tablesJSON)
345
- tablesJSON = await readJSON(
346
- "tables.json",
347
- `${cordova.file.applicationDirectory}${"www"}`
348
- );
349
- await updateDb(tablesJSON);
350
- }
351
- await createSyncInfoTables(config.synchedTables);
352
- await initJwt();
353
- await state.refresh_tables();
354
- await state.refresh_views();
355
- await state.refresh_pages();
356
- await state.refresh_page_groups();
357
- await state.refresh_triggers();
358
- state.mobileConfig.localTableIds = await getTableIds(
359
- config.localUserTables
360
- );
361
- await state.setConfig("base_url", config.server_path);
362
- await initRoutes();
363
- const entryPoint = config.entry_point;
364
- await initI18Next();
365
- await readSiteLogo(state);
366
- state.mobileConfig.networkState = navigator.connection.type;
367
- document.addEventListener(
368
- "offline",
369
- offlineHelper.offlineCallback,
370
- false
371
- );
372
- document.addEventListener(
373
- "online",
374
- offlineHelper.onlineCallback,
375
- false
376
- );
377
- const networkDisabled = state.mobileConfig.networkState === "none";
378
- const jwt = state.mobileConfig.jwt;
379
- const alerts = [];
380
- if ((networkDisabled && jwt) || (await checkJWT(jwt))) {
381
- const mobileConfig = state.mobileConfig;
382
- const decodedJwt = jwt_decode(mobileConfig.jwt);
383
- mobileConfig.user = decodedJwt.user;
384
- // TODO remove these, use 'user' everywhere
385
- mobileConfig.role_id = decodedJwt.user.role_id
386
- ? decodedJwt.user.role_id
387
- : 100;
388
- mobileConfig.user_id = decodedJwt.user.id;
389
- mobileConfig.user_name = decodedJwt.user.email;
390
- mobileConfig.language = decodedJwt.user.language;
391
-
392
- mobileConfig.isPublicUser = false;
393
- await i18next.changeLanguage(mobileConfig.language);
394
- if (mobileConfig.allowOfflineMode) {
395
- const { offlineUser } =
396
- (await offlineHelper.getLastOfflineSession()) || {};
397
- if (networkDisabled) {
398
- if (offlineUser && offlineUser !== mobileConfig.user_name)
399
- throw new Error(
400
- `The offline mode is not available, '${offlineUser}' has not yet uploaded offline data.`
401
- );
402
- else
403
- try {
404
- await offlineHelper.startOfflineMode();
405
- } catch (error) {
406
- throw new Error(
407
- `Neither an internet connection nor the offline mode are available: ${
408
- error.message ? error.message : "Unknown error"
409
- }`
410
- );
411
- }
412
- } else if (offlineUser) {
413
- if (offlineUser === mobileConfig.user_name) {
414
- await offlineHelper.sync();
415
- alerts.push({
416
- type: "info",
417
- msg: "Synchronized your offline data.",
418
- });
419
- } else
420
- alerts.push({
421
- type: "warning",
422
- msg: `'${offlineUser}' has not yet uploaded offline data.`,
423
- });
424
- } else {
425
- await offlineHelper.sync();
426
- alerts.push({
427
- type: "info",
428
- msg: "Synchronized your offline data.",
429
- });
430
- }
431
- }
432
- let page = null;
433
- if (!lastLocation) {
434
- addRoute({ route: entryPoint, query: undefined });
435
- page = await router.resolve({
436
- pathname: entryPoint,
437
- fullWrap: true,
438
- alerts,
439
- });
440
- } else {
441
- addRoute({
442
- route: lastLocation.route,
443
- query: lastLocation.query,
444
- });
445
- page = await router.resolve({
446
- pathname: lastLocation.route,
447
- query: lastLocation.query,
448
- fullWrap: true,
449
- alerts,
450
- });
451
- }
452
- if (page.content) await replaceIframe(page.content, page.isFile);
453
- } else if (isPublicJwt(jwt)) {
454
- const config = state.mobileConfig;
455
- config.user = { role_id: 100, user_name: "public", language: "en" };
456
- // TODO remove these, use 'user' everywhere
457
- config.role_id = 100;
458
- config.user_name = "public";
459
- config.language = "en";
460
-
461
- config.isPublicUser = true;
462
- i18next.changeLanguage(config.language);
463
- addRoute({ route: entryPoint, query: undefined });
464
- const page = await router.resolve({
465
- pathname: entryPoint,
466
- fullWrap: true,
467
- alerts,
468
- });
469
- if (page.content) await replaceIframe(page.content, page.isFile);
470
- } else if (
471
- (await isPublicEntryPoint(entryPoint)) &&
472
- state.mobileConfig.autoPublicLogin
473
- ) {
474
- if (networkDisabled)
475
- throw new Error(
476
- "No internet connection or previous login is available. " +
477
- "Please go online and reload, the public login is not yet supported."
478
- );
479
- await publicLogin(entryPoint);
480
- } else await showLogin(alerts);
481
- } catch (error) {
482
- if (
483
- typeof saltcorn === "undefined" ||
484
- typeof router === "undefined"
485
- ) {
486
- const msg = `An error occured: ${
487
- error.message ? error.message : "Unknown error"
488
- }`;
489
- console.log(msg);
490
- alert(msg);
491
- } else {
492
- if (error.httpCode === 401) await showLogin([]);
493
- else await showErrorPage(error);
494
- }
495
- }
496
- };
497
-
498
- document.addEventListener("deviceready", init);
499
-
8
+ <script src="data/tables.js"></script>
9
+ <script src="data/tables_created_at.js"></script>
10
+ <script src="data/config.js"></script>
11
+ <script src="data/translations.js"></script>
12
+ <script src="data/encoded_site_logo.js"></script>
13
+ <script type="module" src="./dist/bundle.js"></script>
14
+ <script src="js/iframe_view_utils.js"></script>
15
+
16
+ <script type="module">
17
+ import { mobileApp } from "./dist/bundle.js";
18
+ if (window.saltcorn) window.saltcorn.mobileApp = mobileApp;
19
+ else window.saltcorn = { mobileApp };
20
+
21
+ document.addEventListener("deviceready", () =>
22
+ saltcorn.mobileApp.init({
23
+ mobileConfig: _sc_mobile_config,
24
+ tablesSchema: _sc_tables,
25
+ schemaCreatedAt: _sc_tables_created_at,
26
+ translations: _sc_translations,
27
+ siteLogo: _sc_site_logo,
28
+ })
29
+ );
500
30
  document.addEventListener("backbutton", async () => {
501
- await goBack(1, true);
31
+ await saltcorn.mobileApp.navigation.goBack(1, true);
502
32
  });
503
33
  </script>
504
34
  </head>