@saltcorn/mobile-app 1.5.0-beta.8 → 1.5.0-rc.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.
@@ -4,25 +4,32 @@ import { apiCall } from "./api";
4
4
  import { Camera, CameraResultType } from "@capacitor/camera";
5
5
  import { ScreenOrientation } from "@capacitor/screen-orientation";
6
6
  import { SendIntent } from "send-intent";
7
+ import { addPushSyncHandler } from "./offline_mode";
7
8
 
8
9
  const orientationChangeListeners = new Set();
9
10
 
10
- export function clearAlerts() {
11
+ /**
12
+ * Make the toast area shown at the bottom empty
13
+ */
14
+ export function clearToasts() {
11
15
  const iframe = document.getElementById("content-iframe");
12
16
  const alertsArea =
13
17
  iframe.contentWindow.document.getElementById("toasts-area");
14
18
  alertsArea.innerHTML = "";
15
19
  }
16
20
 
17
- export function showAlerts(alerts, toast = true) {
21
+ /**
22
+ * Show toasts in the toast area at the bottom
23
+ * @param toasts
24
+ * @returns
25
+ */
26
+ export function showToasts(toasts) {
18
27
  if (typeof saltcorn === "undefined") {
19
28
  console.log("Not yet initalized.");
20
- console.log(alerts);
29
+ console.log(toasts);
21
30
  } else {
22
31
  const iframe = document.getElementById("content-iframe");
23
- let area = iframe.contentWindow.document.getElementById(
24
- toast ? "toasts-area" : "top-alert"
25
- );
32
+ let area = iframe.contentWindow.document.getElementById("toasts-area");
26
33
  if (!area) {
27
34
  const areaHtml = `<div class="container">
28
35
  <div
@@ -36,18 +43,14 @@ export function showAlerts(alerts, toast = true) {
36
43
  iframe.contentWindow.document
37
44
  .getElementById("page-inner-content")
38
45
  .insertAdjacentHTML("beforeend", areaHtml);
39
- area = iframe.contentWindow.document.getElementById(
40
- toast ? "toasts-area" : "top-alert"
41
- );
46
+ area = iframe.contentWindow.document.getElementById("toasts-area");
42
47
  }
43
48
  const successIds = [];
44
49
  area.innerHTML = "";
45
- for (const { type, msg, title } of alerts) {
46
- if (toast) {
47
- const rndid = `tab${Math.floor(Math.random() * 16777215).toString(16)}`;
48
- area.innerHTML += saltcorn.markup.toast(type, msg, rndid, title);
49
- if (type === "success") successIds.push(rndid);
50
- } else area.innerHTML += saltcorn.markup.alert(type, msg);
50
+ for (const { type, msg, title } of toasts) {
51
+ const rndid = `tab${Math.floor(Math.random() * 16777215).toString(16)}`;
52
+ area.innerHTML += saltcorn.markup.toast(type, msg, rndid, title);
53
+ if (type === "success") successIds.push(rndid);
51
54
  }
52
55
  if (successIds.length > 0) {
53
56
  setTimeout(() => {
@@ -61,6 +64,34 @@ export function showAlerts(alerts, toast = true) {
61
64
  return true;
62
65
  }
63
66
 
67
+ /**
68
+ * Make the alert area at the top empty
69
+ */
70
+ export function clearAlerts() {
71
+ const iframe = document.getElementById("content-iframe");
72
+ const area = iframe.contentWindow.document.getElementById("alerts-area");
73
+ if (area) area.innerHTML = "";
74
+ const topAlert = iframe.contentWindow.document.getElementById("top-alert");
75
+ if (topAlert) topAlert.innerHTML = "";
76
+ }
77
+
78
+ /**
79
+ * Show alerts in the alert area at the top
80
+ * @param alerts
81
+ */
82
+ export function showAlerts(alerts) {
83
+ if (typeof saltcorn === "undefined") {
84
+ console.log("Not yet initalized.");
85
+ console.log(alerts);
86
+ } else {
87
+ for (const { type, msg, title } of alerts) {
88
+ const iframe = document.getElementById("content-iframe");
89
+ const area = iframe.contentWindow.document.getElementById("top-alert");
90
+ area.innerHTML += saltcorn.markup.alert(type, msg);
91
+ }
92
+ }
93
+ }
94
+
64
95
  export function showLoadSpinner() {
65
96
  const iframe = document.getElementById("content-iframe");
66
97
  if (iframe) iframe.contentWindow.showLoadSpinner();
@@ -71,17 +102,9 @@ export function removeLoadSpinner() {
71
102
  if (iframe) iframe.contentWindow.removeLoadSpinner();
72
103
  }
73
104
 
74
- export function clearTopAlerts() {
75
- const iframe = document.getElementById("content-iframe");
76
- const area = iframe.contentWindow.document.getElementById("alerts-area");
77
- if (area) area.innerHTML = "";
78
- const topAlert = iframe.contentWindow.document.getElementById("top-alert");
79
- if (topAlert) topAlert.innerHTML = "";
80
- }
81
-
82
105
  export function errorAlert(error) {
83
106
  console.error(error);
84
- showAlerts([
107
+ showToasts([
85
108
  {
86
109
  type: "error",
87
110
  msg: error.message ? error.message : "An error occured.",
@@ -109,7 +132,7 @@ export async function loadFileAsText(fileId) {
109
132
  });
110
133
  } catch (error) {
111
134
  if (
112
- !showAlerts([
135
+ !showToasts([
113
136
  {
114
137
  type: "error",
115
138
  msg: error.message ? error.message : "An error occured.",
@@ -138,7 +161,7 @@ export async function loadEncodedFile(fileId) {
138
161
  reader.readAsDataURL(response.data);
139
162
  });
140
163
  } catch (error) {
141
- showAlerts([
164
+ showToasts([
142
165
  {
143
166
  type: "error",
144
167
  msg: error.message ? error.message : "An error occured.",
@@ -156,7 +179,7 @@ export async function takePhoto() {
156
179
  });
157
180
  return image.path;
158
181
  } catch (error) {
159
- showAlerts([
182
+ showToasts([
160
183
  {
161
184
  type: "error",
162
185
  msg: error.message ? error.message : "An error occured.",
@@ -189,3 +212,78 @@ export async function checkSendIntentReceived() {
189
212
  return null;
190
213
  }
191
214
  }
215
+
216
+ /**
217
+ * init the push system, if available
218
+ */
219
+ export async function tryInitPush(config) {
220
+ try {
221
+ const { initPushNotifications, addPusNotifyHandler } = await import(
222
+ "../helpers/notifications.js"
223
+ );
224
+ try {
225
+ await initPushNotifications();
226
+ if (saltcorn.data.utils.isPushEnabled(config.user)) addPusNotifyHandler();
227
+ if (config.pushSync) addPushSyncHandler();
228
+ } catch (error) {
229
+ console.error("Error initializing push notifications:", error);
230
+ }
231
+ } catch (error) {
232
+ console.log("Push notifications module not available:", error);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * init background sync, if available
238
+ */
239
+ export async function tryInitBackgroundSync(config) {
240
+ try {
241
+ const { startPeriodicBackgroundSync } = await import(
242
+ "../helpers/background_sync.js"
243
+ );
244
+ try {
245
+ if (config.syncInterval && config.syncInterval > 0)
246
+ await startPeriodicBackgroundSync(config.syncInterval);
247
+ } catch (error) {
248
+ console.error("Error initializing background sync:", error);
249
+ }
250
+ } catch (error) {
251
+ console.log("Background sync module not available:", error);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * end the push system, if available
257
+ */
258
+ export async function tryUnregisterPush() {
259
+ try {
260
+ const { unregisterPushNotifications } = await import(
261
+ "../helpers/notifications.js"
262
+ );
263
+ try {
264
+ await unregisterPushNotifications();
265
+ } catch (error) {
266
+ console.error("Error unregistering push notifications:", error);
267
+ }
268
+ } catch (error) {
269
+ console.log("Push notifications module not available:", error);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * stop background sync, if available
275
+ */
276
+ export async function tryStopBackgroundSync() {
277
+ try {
278
+ const { stopPeriodicBackgroundSync } = await import(
279
+ "../helpers/background_sync.js"
280
+ );
281
+ try {
282
+ await stopPeriodicBackgroundSync();
283
+ } catch (error) {
284
+ console.error("Error stopping periodic background sync:", error);
285
+ }
286
+ } catch (error) {
287
+ console.error("Push notifications module not available:", error);
288
+ }
289
+ }
@@ -3,7 +3,7 @@ import i18next from "i18next";
3
3
 
4
4
  import { router } from "../routing/index";
5
5
  import { startOfflineMode } from "./offline_mode";
6
- import { showAlerts } from "./common";
6
+ import { showToasts } from "./common";
7
7
 
8
8
  export let routingHistory = [];
9
9
 
@@ -186,7 +186,7 @@ export async function handleRoute(route, query, files, data) {
186
186
  await replaceIframeInnerContent(page.content);
187
187
  else await replaceIframe(page.content, page.isFile);
188
188
  } else {
189
- showAlerts([
189
+ showToasts([
190
190
  {
191
191
  type: "warning",
192
192
  msg: i18next.t("%s finished without a result", {
@@ -199,7 +199,7 @@ export async function handleRoute(route, query, files, data) {
199
199
  }
200
200
  } catch (error) {
201
201
  if (routeAdded) popRoute();
202
- showAlerts([
202
+ showToasts([
203
203
  {
204
204
  type: "error",
205
205
  msg: `${i18next.t("In %s", {
@@ -253,7 +253,7 @@ export async function gotoEntryView() {
253
253
  addRoute({ route: mobileConfig.entry_point, query: undefined });
254
254
  await replaceIframeInnerContent(page.content);
255
255
  } catch (error) {
256
- showAlerts([
256
+ showToasts([
257
257
  {
258
258
  type: "error",
259
259
  msg: error.message ? error.message : "An error occured.",
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { apiCall } from "./api";
4
4
  import {
5
- showAlerts,
5
+ showToasts,
6
+ clearToasts,
6
7
  clearAlerts,
8
+ showAlerts,
7
9
  errorAlert,
8
10
  showLoadSpinner,
9
11
  removeLoadSpinner,
@@ -318,6 +320,43 @@ const handleUniqueConflicts = async (uniqueConflicts, translatedIds) => {
318
320
  }
319
321
  };
320
322
 
323
+ /**
324
+ * If there was a field level data conflict, the server version is applied
325
+ * When it also was translated, change the untranslated row and translate it later the normal way
326
+ * @param {*} dataConflicts
327
+ * @param {*} translatedIds
328
+ * @param {*} alerts
329
+ */
330
+ const handleUpdateConflicts = async (dataConflicts, translatedIds, alerts) => {
331
+ let hasConflicts = false;
332
+ for (const [tblName, updates] of Object.entries(dataConflicts)) {
333
+ const table = saltcorn.data.models.Table.findOne({ name: tblName });
334
+ const pkName = table.pk_name || "id";
335
+ const translations = translatedIds[tblName] || {};
336
+ for (const update of updates) {
337
+ // search a translation where the current row is the target
338
+ // and if found, make sure the untranslated row is updated
339
+ for (const [from, to] of Object.entries(translations)) {
340
+ if (to === update[pkName]) {
341
+ update[pkName] = from;
342
+ }
343
+ }
344
+
345
+ const { [pkName]: _sc_pkValue, ...rest } = update;
346
+ await table.updateRow(rest, _sc_pkValue);
347
+ hasConflicts = true;
348
+ }
349
+ }
350
+ if (hasConflicts) {
351
+ alerts.push({
352
+ type: "info",
353
+ msg:
354
+ "Some of your changes could not be applied because the data has changed on the server. " +
355
+ "Your local data has been updated accordingly.",
356
+ });
357
+ }
358
+ };
359
+
321
360
  const updateSyncInfos = async (
322
361
  offlineChanges,
323
362
  allTranslations,
@@ -357,7 +396,7 @@ const updateSyncInfos = async (
357
396
  }
358
397
  };
359
398
 
360
- const syncOfflineData = async (synchedTables, syncTimestamp) => {
399
+ const syncOfflineData = async (synchedTables, syncTimestamp, alerts) => {
361
400
  const offlineChanges = await loadOfflineChanges(synchedTables);
362
401
  if (Object.keys(offlineChanges).length === 0) return null;
363
402
  const uploadResp = await apiCall({
@@ -365,7 +404,8 @@ const syncOfflineData = async (synchedTables, syncTimestamp) => {
365
404
  path: "/sync/offline_changes",
366
405
  body: {
367
406
  changes: offlineChanges,
368
- syncTimestamp,
407
+ oldSyncTimestamp: await getLocalSyncTimestamp(),
408
+ newSyncTimestamp: syncTimestamp,
369
409
  },
370
410
  });
371
411
  const { syncDir } = uploadResp.data;
@@ -377,10 +417,12 @@ const syncOfflineData = async (synchedTables, syncTimestamp) => {
377
417
  path: `/sync/upload_finished?dir_name=${encodeURIComponent(syncDir)}`,
378
418
  });
379
419
  pollCount++;
380
- const { finished, translatedIds, uniqueConflicts, error } = pollResp.data;
420
+ const { finished, translatedIds, uniqueConflicts, dataConflicts, error } =
421
+ pollResp.data;
381
422
  if (finished) {
382
423
  if (error) throw new Error(error.message);
383
424
  else {
425
+ await handleUpdateConflicts(dataConflicts, translatedIds, alerts);
384
426
  await handleUniqueConflicts(uniqueConflicts, translatedIds);
385
427
  await handleTranslatedIds(uniqueConflicts, translatedIds);
386
428
  await updateSyncInfos(offlineChanges, translatedIds, syncTimestamp);
@@ -425,7 +467,7 @@ const checkCleanSync = async (uploadStarted, uploadStartTime, userName) => {
425
467
  return false;
426
468
  };
427
469
 
428
- const getSyncTimestamp = async () => {
470
+ const getServerTime = async () => {
429
471
  const resp = await apiCall({
430
472
  method: "GET",
431
473
  path: `/sync/sync_timestamp`,
@@ -433,6 +475,16 @@ const getSyncTimestamp = async () => {
433
475
  return resp.data.syncTimestamp;
434
476
  };
435
477
 
478
+ const setLocalSyncTimestamp = async (syncTimestamp) => {
479
+ const state = saltcorn.data.state.getState();
480
+ await state.setConfig("mobile_sync_timestamp", syncTimestamp);
481
+ };
482
+
483
+ const getLocalSyncTimestamp = async () => {
484
+ const state = saltcorn.data.state.getState();
485
+ return await state.getConfig("mobile_sync_timestamp");
486
+ };
487
+
436
488
  const setSpinnerText = () => {
437
489
  const iframeWindow = $("#content-iframe")[0].contentWindow;
438
490
  if (iframeWindow) {
@@ -445,57 +497,83 @@ const setSpinnerText = () => {
445
497
  }
446
498
  };
447
499
 
448
- export async function sync() {
449
- setSpinnerText();
450
- const state = saltcorn.data.state.getState();
451
- const { user } = state.mobileConfig;
452
- const { offlineUser, hasOfflineData, uploadStarted, uploadStartTime } =
453
- (await getLastOfflineSession()) || {};
454
- if (offlineUser && hasOfflineData && offlineUser !== user.email) {
455
- throw new Error(
456
- `The sync is not available, '${offlineUser}' has not yet uploaded offline data.`
457
- );
458
- } else {
459
- let syncDir = null;
460
- let cleanSync = await checkCleanSync(
461
- uploadStarted,
462
- uploadStartTime,
463
- user.email
464
- );
465
- const syncTimestamp = await getSyncTimestamp();
466
- await setUploadStarted(true, syncTimestamp);
467
- let lock = null;
468
- try {
469
- if (window.navigator?.wakeLock?.request)
470
- lock = await window.navigator.wakeLock.request();
471
- } catch (error) {
472
- console.log("wakeLock not available");
473
- console.log(error);
474
- }
475
- let transactionOpen = false;
476
- try {
477
- await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
478
- await saltcorn.data.db.query("BEGIN");
479
- transactionOpen = true;
480
- if (cleanSync) await clearLocalData(true);
481
- const { synchedTables, syncInfos } = await prepare();
482
- await syncRemoteDeletes(syncInfos, syncTimestamp);
483
- syncDir = await syncOfflineData(synchedTables, syncTimestamp);
484
- await syncRemoteData(syncInfos, syncTimestamp);
485
- await endOfflineMode(true);
486
- await setUploadStarted(false);
487
- await saltcorn.data.db.query("COMMIT");
488
- transactionOpen = false;
489
- await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
490
- } catch (error) {
491
- if (transactionOpen) await saltcorn.data.db.query("ROLLBACK");
492
- await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
493
- console.log(error);
494
- throw error;
495
- } finally {
496
- if (syncDir) await cleanSyncDir(syncDir);
497
- if (lock) await lock.release();
500
+ let syncInProgress = false;
501
+
502
+ export function isSyncInProgress() {
503
+ return syncInProgress;
504
+ }
505
+
506
+ /**
507
+ *
508
+ * @param {boolean} withWakelock try request a wakelock and show a spinner during sync (propably not in background mode)
509
+ * @param {boolean} switchOnline end the offline mode and go online again after sync
510
+ * @param {string[]} alerts output alerts collected during sync
511
+ */
512
+ export async function sync(
513
+ withWakelock = true,
514
+ switchOnline = true,
515
+ alerts = []
516
+ ) {
517
+ if (syncInProgress)
518
+ throw new Error("A synchronization is already in progress.");
519
+ syncInProgress = true;
520
+ try {
521
+ if (withWakelock) setSpinnerText();
522
+ const state = saltcorn.data.state.getState();
523
+ const { user } = state.mobileConfig;
524
+ const { offlineUser, hasOfflineData, uploadStarted, uploadStartTime } =
525
+ (await getLastOfflineSession()) || {};
526
+ if (offlineUser && hasOfflineData && offlineUser !== user.email) {
527
+ throw new Error(
528
+ `The sync is not available, '${offlineUser}' has not yet uploaded offline data.`
529
+ );
530
+ } else {
531
+ let syncDir = null;
532
+ let cleanSync = await checkCleanSync(
533
+ uploadStarted,
534
+ uploadStartTime,
535
+ user.email
536
+ );
537
+ const syncTimestamp = await getServerTime();
538
+ await setUploadStarted(true, syncTimestamp);
539
+ let lock = null;
540
+ if (withWakelock) {
541
+ try {
542
+ if (window.navigator?.wakeLock?.request)
543
+ lock = await window.navigator.wakeLock.request();
544
+ } catch (error) {
545
+ console.log("wakeLock not available");
546
+ console.log(error);
547
+ }
548
+ }
549
+ let transactionOpen = false;
550
+ try {
551
+ await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
552
+ await saltcorn.data.db.query("BEGIN");
553
+ transactionOpen = true;
554
+ if (cleanSync) await clearLocalData(true);
555
+ const { synchedTables, syncInfos } = await prepare();
556
+ await syncRemoteDeletes(syncInfos, syncTimestamp);
557
+ syncDir = await syncOfflineData(synchedTables, syncTimestamp, alerts);
558
+ await syncRemoteData(syncInfos, syncTimestamp);
559
+ await setLocalSyncTimestamp(syncTimestamp);
560
+ if (switchOnline) await endOfflineMode(true);
561
+ await setUploadStarted(false);
562
+ await saltcorn.data.db.query("COMMIT");
563
+ transactionOpen = false;
564
+ await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
565
+ } catch (error) {
566
+ if (transactionOpen) await saltcorn.data.db.query("ROLLBACK");
567
+ await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
568
+ console.log(error);
569
+ throw error;
570
+ } finally {
571
+ if (syncDir) await cleanSyncDir(syncDir);
572
+ if (lock) await lock.release();
573
+ }
498
574
  }
575
+ } finally {
576
+ syncInProgress = false;
499
577
  }
500
578
  }
501
579
 
@@ -576,21 +654,50 @@ export async function clearLocalData(inTransaction) {
576
654
  }
577
655
  }
578
656
 
579
- export function networkChangeCallback(status) {
657
+ export async function networkChangeCallback(status) {
580
658
  console.log("Network status changed", status);
581
659
  const mobileConfig = saltcorn.data.state.getState().mobileConfig;
582
660
  if (status.connectionType !== "none" && mobileConfig.isOfflineMode) {
583
661
  const iframeWindow = $("#content-iframe")[0].contentWindow;
584
- if (iframeWindow) {
585
- clearAlerts();
586
- iframeWindow.notifyAlert(
587
- `An internet connection is available, to end the offline mode click ${saltcorn.markup.a(
588
- {
589
- href: "javascript:execLink('/sync/sync_settings')",
590
- },
591
- "here"
592
- )}`
593
- );
662
+ if (mobileConfig.syncOnReconnect) {
663
+ if (isSyncInProgress()) {
664
+ console.log("Sync already in progress, skipping automatic sync");
665
+ return;
666
+ }
667
+ console.log("Network restored, starting automatic sync");
668
+ const toasts = [];
669
+ try {
670
+ await sync(false, true, toasts);
671
+ if (iframeWindow) {
672
+ clearAlerts();
673
+ showAlerts([
674
+ {
675
+ type: "info",
676
+ msg: "You are online again.",
677
+ },
678
+ ]);
679
+ }
680
+ toasts.push({
681
+ type: "info",
682
+ msg: "Synchronized your offline data.",
683
+ });
684
+ } catch (error) {
685
+ console.log("Error during push sync:", error);
686
+ }
687
+ if (toasts.length > 0) showToasts(toasts);
688
+ } else {
689
+ const iframeWindow = $("#content-iframe")[0].contentWindow;
690
+ if (iframeWindow) {
691
+ clearToasts();
692
+ iframeWindow.notifyAlert(
693
+ `An internet connection is available, to end the offline mode click ${saltcorn.markup.a(
694
+ {
695
+ href: "javascript:execLink('/sync/sync_settings')",
696
+ },
697
+ "here"
698
+ )}`
699
+ );
700
+ }
594
701
  }
595
702
  }
596
703
  mobileConfig.networkState = status.connectionType;
@@ -602,7 +709,7 @@ export async function hasOfflineRows() {
602
709
  const table = saltcorn.data.models.Table.findOne({ name: tblName });
603
710
  const pkName = table.pk_name;
604
711
  const { rows } = await saltcorn.data.db.query(
605
- `select count(info_tbl.ref)
712
+ `select count(info_tbl.ref) as total
606
713
  from "${saltcorn.data.db.sqlsanitize(
607
714
  tblName
608
715
  )}_sync_info" as info_tbl
@@ -610,7 +717,7 @@ export async function hasOfflineRows() {
610
717
  on info_tbl.ref = data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}"
611
718
  where info_tbl.modified_local = true`
612
719
  );
613
- if (rows?.length > 0 && parseInt(rows[0].count) > 0) return true;
720
+ if (rows?.length > 0 && parseInt(rows[0].total) > 0) return true;
614
721
  }
615
722
  return false;
616
723
  }
@@ -630,7 +737,7 @@ export async function deleteOfflineData(noFeedback) {
630
737
  await clearLocalData(false);
631
738
  await setHasOfflineData(false);
632
739
  if (!noFeedback)
633
- showAlerts([
740
+ showToasts([
634
741
  {
635
742
  type: "info",
636
743
  msg: "Deleted your offline data.",
@@ -643,3 +750,17 @@ export async function deleteOfflineData(noFeedback) {
643
750
  if (!noFeedback) removeLoadSpinner();
644
751
  }
645
752
  }
753
+
754
+ export function addPushSyncHandler() {
755
+ const state = saltcorn.data.state.getState();
756
+ state.mobile_push_handler["push_sync"] = async (notification) => {
757
+ console.log("Push sync received:", notification);
758
+ const alerts = [];
759
+ try {
760
+ await sync(false, false, alerts);
761
+ if (alerts.length > 0) showToasts(alerts);
762
+ } catch (error) {
763
+ console.log("Error during push sync:", error);
764
+ }
765
+ };
766
+ }
package/src/index.js CHANGED
@@ -2,13 +2,32 @@ import { init } from "./init";
2
2
  import * as api from "./helpers/api";
3
3
  import * as auth from "./helpers/auth";
4
4
  import * as common from "./helpers/common";
5
- import * as notifications from "./helpers/notifications";
6
5
  import * as fileSystem from "./helpers/file_system";
7
6
  import * as navigation from "./helpers/navigation";
8
7
  import * as offlineMode from "./helpers/offline_mode";
9
8
  import * as dbSchema from "./helpers/db_schema";
10
9
  import { router } from "./routing/index";
11
10
 
11
+ // included when push notifications or push sync is enabled
12
+ let notifications = undefined;
13
+ try {
14
+ notifications = require("./helpers/notifications");
15
+ console.log("Notifications module available");
16
+ } catch (err) {
17
+ console.log("Notifications module not available");
18
+ }
19
+
20
+ // included when periodic background sync is enabled
21
+ let backgroundSync = undefined;
22
+ try {
23
+ backgroundSync = require("./helpers/background_sync");
24
+ console.log("Background sync module available");
25
+ }
26
+ catch (err) {
27
+ console.log("Background sync module not available");
28
+ }
29
+
30
+ // include code placed in a mobile-app directory inside plugins
12
31
  const plugins = {};
13
32
  const context = require.context("./plugins-code", true, /index\.js$/);
14
33
  context.keys().forEach((key) => {
@@ -22,10 +41,11 @@ export const mobileApp = {
22
41
  api,
23
42
  auth,
24
43
  common,
25
- notifications,
26
44
  fileSystem,
27
45
  navigation: { ...navigation, router },
28
46
  offlineMode,
29
47
  dbSchema,
48
+ ...(notifications ? { notifications } : {}),
49
+ ...(backgroundSync ? { backgroundSync } : {}),
30
50
  plugins,
31
51
  };