@saltcorn/mobile-app 1.6.0 → 1.7.0-alpha.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.
@@ -0,0 +1,134 @@
1
+ import { BackgroundFetch } from "@transistorsoft/capacitor-background-fetch";
2
+ import { performSync, performHeartbeat } from "./background_tasks.js";
3
+
4
+ const SYNC_TASK_ID = "com.transistorsoft.background_sync";
5
+ const HEARTBEAT_TASK_ID = "com.transistorsoft.push_sync_heartbeat";
6
+
7
+ let isConfigured = false;
8
+
9
+ /**
10
+ * Call BackgroundFetch.configure() with a task-routing callback.
11
+ * Sets isConfigured on success.
12
+ * @param {number} minimumFetchInterval hint to the OS in minutes
13
+ */
14
+ async function configure(minimumFetchInterval) {
15
+ const status = await BackgroundFetch.configure(
16
+ { minimumFetchInterval },
17
+ async (taskId) => {
18
+ switch (taskId) {
19
+ case SYNC_TASK_ID:
20
+ try {
21
+ await performSync();
22
+ } catch (e) {
23
+ console.error("Background sync error:", e);
24
+ }
25
+ break;
26
+ case HEARTBEAT_TASK_ID:
27
+ try {
28
+ await performHeartbeat();
29
+ } catch (e) {
30
+ console.error("Heartbeat error:", e);
31
+ }
32
+ break;
33
+ default:
34
+ console.log("[BackgroundFetch] unknown task:", taskId);
35
+ }
36
+ BackgroundFetch.finish(taskId);
37
+ },
38
+ async (taskId) => {
39
+ console.log("[BackgroundFetch] TIMEOUT:", taskId);
40
+ BackgroundFetch.finish(taskId);
41
+ }
42
+ );
43
+
44
+ if (status === BackgroundFetch.STATUS_AVAILABLE) {
45
+ isConfigured = true;
46
+ console.log("BackgroundFetch configured successfully");
47
+ } else if (status === BackgroundFetch.STATUS_DENIED) {
48
+ console.log("BackgroundFetch: user denied background behavior");
49
+ } else if (status === BackgroundFetch.STATUS_RESTRICTED) {
50
+ console.log("BackgroundFetch: background updates restricted");
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Schedule the configured named tasks via BackgroundFetch.scheduleTask.
56
+ * @param {number|undefined} syncInterval background-sync interval in minutes
57
+ * @param {number|undefined} heartbeatInterval push-sync heartbeat interval in minutes
58
+ */
59
+ async function scheduleTasks(syncInterval, heartbeatInterval) {
60
+ if (syncInterval > 0) {
61
+ try {
62
+ await BackgroundFetch.scheduleTask({
63
+ taskId: SYNC_TASK_ID,
64
+ delay: syncInterval * 60 * 1000,
65
+ periodic: true,
66
+ requiresNetworkConnectivity: true,
67
+ });
68
+ console.log(
69
+ `Background sync scheduled, interval: ${syncInterval} minute(s)`
70
+ );
71
+ } catch (e) {
72
+ console.error("Error scheduling sync task:", e);
73
+ }
74
+ }
75
+
76
+ if (heartbeatInterval > 0) {
77
+ try {
78
+ await BackgroundFetch.scheduleTask({
79
+ taskId: HEARTBEAT_TASK_ID,
80
+ delay: heartbeatInterval * 60 * 1000,
81
+ periodic: true,
82
+ requiresNetworkConnectivity: true,
83
+ });
84
+ console.log(
85
+ `Push sync heartbeat scheduled, interval: ${heartbeatInterval} minute(s)`
86
+ );
87
+ } catch (e) {
88
+ console.error("Error scheduling heartbeat task:", e);
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Init background tasks. Routes task IDs to performSync / performHeartbeat
95
+ * from background_tasks.js. Only tasks with a configured interval are scheduled.
96
+ * @param {number|undefined} syncInterval background-sync interval in minutes
97
+ * @param {number|undefined} heartbeatInterval push-sync heartbeat interval in minutes
98
+ * @returns {Promise<boolean>} true if BackgroundFetch was configured successfully
99
+ */
100
+ export async function initBackground(syncInterval, heartbeatInterval) {
101
+ const hasSyncInterval = syncInterval && syncInterval > 0;
102
+ const hasHeartbeatInterval = heartbeatInterval && heartbeatInterval > 0;
103
+
104
+ if (!hasSyncInterval && !hasHeartbeatInterval) {
105
+ console.log("initBackground: no intervals configured, skipping");
106
+ return false;
107
+ }
108
+
109
+ if (!isConfigured) {
110
+ const minimumFetchInterval = Math.min(
111
+ hasSyncInterval ? syncInterval : Infinity,
112
+ hasHeartbeatInterval ? heartbeatInterval : Infinity
113
+ );
114
+ await configure(minimumFetchInterval);
115
+ }
116
+
117
+ if (isConfigured) await scheduleTasks(syncInterval, heartbeatInterval);
118
+
119
+ return isConfigured;
120
+ }
121
+
122
+ /**
123
+ * Stop all background tasks (sync + heartbeat). Call on logout.
124
+ */
125
+ export async function stopBackground() {
126
+ if (!isConfigured) return;
127
+ try {
128
+ await BackgroundFetch.stop();
129
+ isConfigured = false;
130
+ console.log("Background tasks stopped");
131
+ } catch (e) {
132
+ console.error("Error stopping background tasks:", e);
133
+ }
134
+ }
@@ -0,0 +1,31 @@
1
+ /*global saltcorn*/
2
+
3
+ import { CapacitorSilentNotifications } from "capacitor-plugin-silent-notifications";
4
+
5
+ let silentNotificationListener = null;
6
+
7
+ export async function initIosSilentPushListener() {
8
+ if (silentNotificationListener) {
9
+ await silentNotificationListener.remove();
10
+ silentNotificationListener = null;
11
+ }
12
+ silentNotificationListener = await CapacitorSilentNotifications.addListener(
13
+ "silentNotificationReceived",
14
+ async (notification) => {
15
+ console.log("iOS silent push received:", notification);
16
+ const state = saltcorn.data.state.getState();
17
+ if (state.mobile_push_handler?.["push_sync"]) {
18
+ try {
19
+ await state.mobile_push_handler["push_sync"](notification);
20
+ } catch (error) {
21
+ console.error("Error handling push_sync silent notification:", error);
22
+ }
23
+ }
24
+ }
25
+ );
26
+ }
27
+
28
+ export async function removeIosSilentPushListener() {
29
+ await silentNotificationListener?.remove();
30
+ silentNotificationListener = null;
31
+ }
@@ -11,28 +11,40 @@ import { Device } from "@capacitor/device";
11
11
  */
12
12
  async function notifyTokenApi(config, isSubscribe) {
13
13
  console.log("notifyTokenApi subscribe:", isSubscribe);
14
+ if (!config.pushConfiguration) {
15
+ console.warn("notifyTokenApi: pushConfiguration is not set, skipping");
16
+ return;
17
+ }
14
18
  const { token, deviceId } = config.pushConfiguration;
15
19
  const platform = Capacitor.getPlatform();
16
20
  try {
17
21
  const response = await apiCall({
18
22
  method: "POST",
19
- path: `/notifications/mobile-${isSubscribe ? "subscribe" : "remove-subscription"}`,
23
+ path: `/notifications/mobile-${
24
+ isSubscribe ? "subscribe" : "remove-subscription"
25
+ }`,
20
26
  body: { token, deviceId, platform },
21
27
  });
22
28
  const data = response.data;
23
29
  if (data.success === "ok")
24
30
  console.log(
25
- `successfully ${isSubscribe ? "subscribed" : "unsubscribed"} to push notifications`,
31
+ `successfully ${
32
+ isSubscribe ? "subscribed" : "unsubscribed"
33
+ } to push notifications`,
26
34
  data
27
35
  );
28
36
  else
29
37
  console.error(
30
- `unable to ${isSubscribe ? "subscribe" : "unsubscribe"} to push notifications`,
38
+ `unable to ${
39
+ isSubscribe ? "subscribe" : "unsubscribe"
40
+ } to push notifications`,
31
41
  data
32
42
  );
33
43
  } catch (error) {
34
44
  console.error(
35
- `unable to ${isSubscribe ? "subscribe" : "unsubscribe"} to push notifications`,
45
+ `unable to ${
46
+ isSubscribe ? "subscribe" : "unsubscribe"
47
+ } to push notifications`,
36
48
  error
37
49
  );
38
50
  }
@@ -45,18 +57,30 @@ async function notifyTokenApi(config, isSubscribe) {
45
57
  */
46
58
  async function syncTokenApi(config, isSubscribe) {
47
59
  console.log("syncTokenApi subscribe:", isSubscribe);
60
+ if (!config.pushConfiguration) {
61
+ console.warn("syncTokenApi: pushConfiguration is not set, skipping");
62
+ return;
63
+ }
48
64
  const { token, deviceId } = config.pushConfiguration;
49
65
  const platform = Capacitor.getPlatform();
50
66
  try {
51
67
  const response = await apiCall({
52
68
  method: "POST",
53
69
  path: `/sync/push_${isSubscribe ? "subscribe" : "unsubscribe"}`,
54
- body: { token, deviceId, synchedTables: config.synchedTables, platform },
70
+ body: {
71
+ token,
72
+ deviceId,
73
+ synchedTables: config.synchedTables,
74
+ platform,
75
+ apnsEnvironment: config.apnsEnvironment,
76
+ },
55
77
  });
56
78
  const data = response.data;
57
79
  if (data.success === "ok")
58
80
  console.log(
59
- `successfully ${isSubscribe ? "subscribed" : "unsubscribed"} to push sync`,
81
+ `successfully ${
82
+ isSubscribe ? "subscribed" : "unsubscribed"
83
+ } to push sync`,
60
84
  data
61
85
  );
62
86
  else
@@ -94,7 +118,6 @@ export async function initPushNotifications() {
94
118
  await removePushListeners();
95
119
  const permStatus = await PushNotifications.requestPermissions();
96
120
  if (permStatus.receive === "granted") {
97
- await PushNotifications.register();
98
121
  registrationListener = PushNotifications.addListener(
99
122
  "registration",
100
123
  async (token) => {
@@ -109,8 +132,7 @@ export async function initPushNotifications() {
109
132
  deviceId: identifier,
110
133
  };
111
134
  await notifyTokenApi(config, true);
112
- if (config.allowOfflineMode && config.pushSync)
113
- await syncTokenApi(config, true);
135
+ if (config.pushSync) await syncTokenApi(config, true);
114
136
  }
115
137
  }
116
138
  );
@@ -126,6 +148,8 @@ export async function initPushNotifications() {
126
148
  "pushNotificationReceived",
127
149
  messageHandler
128
150
  );
151
+
152
+ await PushNotifications.register();
129
153
  } else {
130
154
  console.warn("Push notification permission not granted");
131
155
  }
@@ -134,17 +158,29 @@ export async function initPushNotifications() {
134
158
 
135
159
  export async function unregisterPushNotifications() {
136
160
  if (Capacitor.getPlatform() !== "web" && PushNotifications) {
161
+ const config = saltcorn.data.state.getState().mobileConfig;
137
162
  try {
138
163
  await PushNotifications.unregister();
139
- const config = saltcorn.data.state.getState().mobileConfig;
140
164
  await notifyTokenApi(config, false);
141
- if (config.allowOfflineMode && config.pushSync)
165
+ if (config.pushSync) {
142
166
  await syncTokenApi(config, false);
143
- await removePushListeners();
144
- config.pushConfiguration = null;
167
+ if (Capacitor.getPlatform() === "ios") {
168
+ try {
169
+ const { removeIosSilentPushListener } = await import(
170
+ "../helpers/ios_silent_push.js"
171
+ );
172
+ await removeIosSilentPushListener();
173
+ } catch (e) {
174
+ console.log("iOS silent push not available:", e);
175
+ }
176
+ }
177
+ }
145
178
  console.log("Push notifications unregistered successfully");
146
179
  } catch (error) {
147
180
  console.error("Error unregistering push notifications:", error);
181
+ } finally {
182
+ await removePushListeners();
183
+ config.pushConfiguration = null;
148
184
  }
149
185
  }
150
186
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "1.6.0",
4
+ "version": "1.7.0-alpha.0",
5
5
  "description": "Saltcorn mobile app for Android and iOS",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -0,0 +1,47 @@
1
+ /*global saltcorn*/
2
+
3
+ import { Capacitor } from "@capacitor/core";
4
+ import { apiCall } from "./api.js";
5
+ import { sync } from "./offline_mode.js";
6
+
7
+ /**
8
+ * Perform one background sync pass.
9
+ */
10
+ export async function performSync() {
11
+ console.log("Background sync starting:", new Date().toISOString());
12
+ await sync(true);
13
+ console.log("Background sync finished:", new Date().toISOString());
14
+ }
15
+
16
+ /**
17
+ * Re-register the push sync subscription with the server.
18
+ */
19
+ export async function performHeartbeat() {
20
+ const config = saltcorn.data.state.getState().mobileConfig;
21
+ if (!config?.pushSync || !config?.pushConfiguration) {
22
+ console.log("Push sync heartbeat: skipping (push sync not active)");
23
+ return;
24
+ }
25
+ const { token, deviceId } = config.pushConfiguration;
26
+ const platform = Capacitor.getPlatform();
27
+ console.log("Push sync heartbeat: re-registering subscription");
28
+ try {
29
+ const response = await apiCall({
30
+ method: "POST",
31
+ path: "/sync/push_subscribe",
32
+ body: {
33
+ token,
34
+ deviceId,
35
+ platform,
36
+ synchedTables: config.synchedTables,
37
+ apnsEnvironment: config.apnsEnvironment,
38
+ },
39
+ });
40
+ const data = response.data;
41
+ if (data.success === "ok")
42
+ console.log("Push sync heartbeat: subscription refreshed");
43
+ else console.error("Push sync heartbeat: server error", data);
44
+ } catch (e) {
45
+ console.error("Push sync heartbeat error:", e);
46
+ }
47
+ }
@@ -1,6 +1,7 @@
1
1
  /*global saltcorn, $*/
2
2
 
3
3
  import { apiCall } from "./api";
4
+ import { Capacitor } from "@capacitor/core";
4
5
  import { Camera, CameraResultType } from "@capacitor/camera";
5
6
  import { ScreenOrientation } from "@capacitor/screen-orientation";
6
7
  import { SendIntent } from "send-intent";
@@ -224,7 +225,19 @@ export async function tryInitPush(config) {
224
225
  try {
225
226
  await initPushNotifications();
226
227
  if (saltcorn.data.utils.isPushEnabled(config.user)) addPusNotifyHandler();
227
- if (config.pushSync) addPushSyncHandler();
228
+ if (config.pushSync) {
229
+ addPushSyncHandler();
230
+ if (Capacitor.getPlatform() === "ios") {
231
+ try {
232
+ const { initIosSilentPushListener } = await import(
233
+ "../helpers/ios_silent_push.js"
234
+ );
235
+ await initIosSilentPushListener();
236
+ } catch (e) {
237
+ console.log("iOS silent push not available:", e);
238
+ }
239
+ }
240
+ }
228
241
  } catch (error) {
229
242
  console.error("Error initializing push notifications:", error);
230
243
  }
@@ -234,21 +247,26 @@ export async function tryInitPush(config) {
234
247
  }
235
248
 
236
249
  /**
237
- * init background sync, if available
250
+ * init background tasks (sync and/or push-sync heartbeat), if available
238
251
  */
239
252
  export async function tryInitBackgroundSync(config) {
253
+ if (
254
+ !(config.syncInterval && config.syncInterval > 0) &&
255
+ !(config.pushSyncHeartbeatInterval && config.pushSyncHeartbeatInterval > 0)
256
+ )
257
+ return;
240
258
  try {
241
- const { startPeriodicBackgroundSync } = await import(
242
- "../helpers/background_sync.js"
243
- );
259
+ const { initBackground } = await import("../helpers/background.js");
244
260
  try {
245
- if (config.syncInterval && config.syncInterval > 0)
246
- await startPeriodicBackgroundSync(config.syncInterval);
261
+ await initBackground(
262
+ config.syncInterval,
263
+ config.pushSyncHeartbeatInterval
264
+ );
247
265
  } catch (error) {
248
- console.error("Error initializing background sync:", error);
266
+ console.error("Error initializing background tasks:", error);
249
267
  }
250
268
  } catch (error) {
251
- console.log("Background sync module not available:", error);
269
+ console.log("Background module not available:", error);
252
270
  }
253
271
  }
254
272
 
@@ -271,19 +289,17 @@ export async function tryUnregisterPush() {
271
289
  }
272
290
 
273
291
  /**
274
- * stop background sync, if available
292
+ * stop background tasks (sync and/or push-sync heartbeat), if available
275
293
  */
276
294
  export async function tryStopBackgroundSync() {
277
295
  try {
278
- const { stopPeriodicBackgroundSync } = await import(
279
- "../helpers/background_sync.js"
280
- );
296
+ const { stopBackground } = await import("../helpers/background.js");
281
297
  try {
282
- await stopPeriodicBackgroundSync();
298
+ await stopBackground();
283
299
  } catch (error) {
284
- console.error("Error stopping periodic background sync:", error);
300
+ console.error("Error stopping background tasks:", error);
285
301
  }
286
302
  } catch (error) {
287
- console.error("Push notifications module not available:", error);
303
+ console.log("Background module not available:", error);
288
304
  }
289
305
  }
@@ -1,16 +0,0 @@
1
- require 'xcodeproj'
2
-
3
- project_path = 'ios/App/App.xcodeproj'
4
- target_name = 'App'
5
- entitlements_relative_path = 'App/App.entitlements'
6
-
7
- project = Xcodeproj::Project.open(project_path)
8
- target = project.targets.find { |t| t.name == target_name }
9
- abort("Target '#{target_name}' not found") unless target
10
-
11
- target.build_configurations.each do |config|
12
- config.build_settings['CODE_SIGN_ENTITLEMENTS'] = entitlements_relative_path
13
- end
14
-
15
- project.save
16
- puts "Entitlements configured successfully for target '#{target_name}'"
@@ -1,91 +0,0 @@
1
- import { BackgroundFetch } from "@transistorsoft/capacitor-background-fetch";
2
- import { sync } from "./offline_mode.js";
3
-
4
- let isConfigured = false;
5
-
6
- /**
7
- * Init the periodic background sync with a min interval.
8
- * This runs the sync even if the app is in background or was swipe closed.
9
- * If no internet connection is available it fails silently.
10
- * @param {number} interval min time interval in minutes. The system decides when to actually do it
11
- * @returns {Promise<boolean>} True if configuration was successful, false otherwise.
12
- */
13
- export async function startPeriodicBackgroundSync(interval = 15) {
14
- if (isConfigured) {
15
- console.log("Background sync is already configured. Skipping.");
16
- return true;
17
- }
18
-
19
- console.log("Configuring background sync with interval (minutes):", interval);
20
-
21
- const status = await BackgroundFetch.configure(
22
- {
23
- minimumFetchInterval: interval,
24
- },
25
- async (taskId) => {
26
- console.log(
27
- "Starting background sync:",
28
- taskId,
29
- new Date().toISOString()
30
- );
31
- await sync(true);
32
- console.log(
33
- "Background sync finished:",
34
- taskId,
35
- new Date().toISOString()
36
- );
37
- BackgroundFetch.finish(taskId);
38
- },
39
- async (taskId) => {
40
- console.log("[BackgroundFetch] TIMEOUT:", taskId);
41
- BackgroundFetch.finish(taskId);
42
- }
43
- );
44
-
45
- if (status === BackgroundFetch.STATUS_AVAILABLE) {
46
- console.log("Background sync successfully configured.");
47
- isConfigured = true; // Set the flag only on success
48
- return true;
49
- } else {
50
- // Handle error statuses
51
- if (status === BackgroundFetch.STATUS_DENIED) {
52
- console.log(
53
- "The user explicitly disabled background behavior for this app or for the whole system."
54
- );
55
- } else if (status === BackgroundFetch.STATUS_RESTRICTED) {
56
- console.log(
57
- "Background updates are unavailable and the user cannot enable them again."
58
- );
59
- }
60
- isConfigured = false;
61
- return false;
62
- }
63
- }
64
-
65
- /**
66
- * Stops/Unregisters the periodic background sync.
67
- * This should be called when the user logs out.
68
- */
69
- export async function stopPeriodicBackgroundSync() {
70
- if (!isConfigured) {
71
- console.log("Background sync is not currently configured. Skipping stop.");
72
- return;
73
- }
74
-
75
- console.log("Stopping background sync");
76
- try {
77
- await BackgroundFetch.stop();
78
- console.log("Background sync successfully stopped.");
79
- isConfigured = false;
80
- } catch (error) {
81
- console.error("Error stopping background sync:", error);
82
- }
83
- }
84
-
85
- /**
86
- * Check if the background sync is currently configured/enabled.
87
- * @returns {boolean} True if the sync is configured, false otherwise.
88
- */
89
- export function isBackgroundSyncActive() {
90
- return isConfigured;
91
- }