@saltcorn/mobile-app 1.6.3-beta.1 → 1.7.0-alpha.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.
- package/optional_sources/background.js +134 -0
- package/optional_sources/ios_silent_push.js +31 -0
- package/optional_sources/notifications.js +49 -13
- package/package.json +1 -2
- package/share_extension_files/Info.plist +20 -2
- package/share_extension_files/ShareViewController.swift +117 -88
- package/src/helpers/api.js +67 -35
- package/src/helpers/auth.js +127 -116
- package/src/helpers/background_tasks.js +47 -0
- package/src/helpers/common.js +32 -16
- package/src/helpers/db_schema.js +6 -4
- package/src/helpers/file_system.js +3 -0
- package/src/helpers/offline_mode.js +9 -13
- package/src/index.js +5 -5
- package/src/init.js +41 -42
- package/src/routing/mocks/request.js +1 -1
- package/src/routing/routes/notifications.js +34 -10
- package/src/routing/routes/view.js +9 -3
- package/src/routing/utils.js +1 -1
- package/www/js/iframe_view_utils.js +65 -18
- package/add_entitlements.rb +0 -16
- package/optional_sources/background_sync.js +0 -91
package/src/helpers/api.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/*global saltcorn, splashConfig*/
|
|
2
2
|
|
|
3
|
-
import axios from "axios";
|
|
4
|
-
import { jwtDecode } from "jwt-decode";
|
|
5
3
|
import { router } from "../routing/index";
|
|
6
4
|
import { replaceIframe, clearHistory } from "./navigation";
|
|
7
5
|
|
|
@@ -19,49 +17,83 @@ export async function apiCall({
|
|
|
19
17
|
? saltcorn.data.state.getState().mobileConfig
|
|
20
18
|
: splashConfig;
|
|
21
19
|
const serverPath = config.server_path;
|
|
22
|
-
|
|
20
|
+
let url = `${serverPath}${path}`;
|
|
21
|
+
if (params) {
|
|
22
|
+
const qs = new URLSearchParams(params).toString();
|
|
23
|
+
if (qs) url += `${url.includes("?") ? "&" : "?"}${qs}`;
|
|
24
|
+
}
|
|
23
25
|
const headers = {
|
|
24
26
|
"X-Requested-With": "XMLHttpRequest",
|
|
25
27
|
"X-Saltcorn-Client": "mobile-app",
|
|
26
28
|
...(additionalHeaders || {}),
|
|
27
29
|
};
|
|
28
30
|
if (config.tenantAppName) headers["X-Saltcorn-App"] = config.tenantAppName;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await saltcorn.data.db.deleteWhere("jwt_table");
|
|
37
|
-
clearHistory();
|
|
38
|
-
const page = await router.resolve({
|
|
39
|
-
pathname: "get/auth/login",
|
|
40
|
-
alerts: [
|
|
41
|
-
{ type: "warning", msg: "Your session has expired, please log in again." },
|
|
42
|
-
],
|
|
43
|
-
});
|
|
44
|
-
await replaceIframe(page.content);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
// malformed token, let the request proceed and fail naturally
|
|
49
|
-
}
|
|
50
|
-
headers.Authorization = `jwt ${token}`;
|
|
31
|
+
// The session cookie is attached automatically by the WebView (credentials: "include").
|
|
32
|
+
// The CSRF token has to be sent explicitly, same as the web client does.
|
|
33
|
+
if (config.csrfToken) headers["CSRF-Token"] = config.csrfToken;
|
|
34
|
+
const fetchOptions = { method, headers, credentials: "include" };
|
|
35
|
+
if (body !== undefined) {
|
|
36
|
+
fetchOptions.body = JSON.stringify(body);
|
|
37
|
+
headers["Content-Type"] = "application/json";
|
|
51
38
|
}
|
|
39
|
+
const controller = timeout ? new AbortController() : null;
|
|
40
|
+
const timeoutId = controller
|
|
41
|
+
? setTimeout(() => controller.abort(), timeout)
|
|
42
|
+
: null;
|
|
43
|
+
if (controller) fetchOptions.signal = controller.signal;
|
|
52
44
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
const res = await fetch(url, fetchOptions);
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const error = new Error(`Request failed with status code ${res.status}`);
|
|
48
|
+
error.response = { status: res.status };
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
let data;
|
|
52
|
+
if (responseType === "blob") data = await res.blob();
|
|
53
|
+
else if (responseType === "text") data = await res.text();
|
|
54
|
+
else {
|
|
55
|
+
// A redirected error page still counts as a 200 here, so parse by
|
|
56
|
+
// hand to give a clear error instead of a cryptic JSON parse failure.
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
try {
|
|
59
|
+
data = text ? JSON.parse(text) : undefined;
|
|
60
|
+
} catch {
|
|
61
|
+
const error = new Error(
|
|
62
|
+
`Expected JSON but got ${
|
|
63
|
+
res.headers.get("content-type") || "unknown content-type"
|
|
64
|
+
} (status ${res.status}, url ${res.url})`
|
|
65
|
+
);
|
|
66
|
+
error.response = { status: res.status };
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { data, status: res.status };
|
|
63
71
|
} catch (error) {
|
|
72
|
+
if (
|
|
73
|
+
error.response?.status === 401 &&
|
|
74
|
+
typeof saltcorn !== "undefined" &&
|
|
75
|
+
!path.startsWith("/auth/")
|
|
76
|
+
) {
|
|
77
|
+
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
78
|
+
mobileConfig.csrfToken = undefined;
|
|
79
|
+
mobileConfig.hasSession = false;
|
|
80
|
+
await saltcorn.data.db.deleteWhere("session_table");
|
|
81
|
+
clearHistory();
|
|
82
|
+
const page = await router.resolve({
|
|
83
|
+
pathname: "get/auth/login",
|
|
84
|
+
alerts: [
|
|
85
|
+
{
|
|
86
|
+
type: "warning",
|
|
87
|
+
msg: "Your session has expired, please log in again.",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
await replaceIframe(page.content);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
64
94
|
error.message = `Unable to call ${method} ${url}:\n${error.message}`;
|
|
65
95
|
throw error;
|
|
96
|
+
} finally {
|
|
97
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
66
98
|
}
|
|
67
99
|
}
|
package/src/helpers/auth.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/*global saltcorn*/
|
|
2
2
|
|
|
3
|
-
import { jwtDecode } from "jwt-decode";
|
|
4
3
|
import i18next from "i18next";
|
|
5
4
|
import { apiCall } from "./api";
|
|
6
5
|
import { router } from "../routing/index";
|
|
@@ -15,50 +14,57 @@ import {
|
|
|
15
14
|
} from "./common";
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
17
|
+
* fetches a CSRF token and caches it in mobileConfig.csrfToken.
|
|
18
|
+
* Call again after every login/logout, since both reset it.
|
|
19
|
+
*/
|
|
20
|
+
export async function refreshCsrfToken() {
|
|
21
|
+
const response = await apiCall({ method: "GET", path: "/auth/csrf-token" });
|
|
22
|
+
const config = saltcorn.data.state.getState().mobileConfig;
|
|
23
|
+
config.csrfToken = response?.data?.csrfToken;
|
|
24
|
+
return config.csrfToken;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* internal helper for the normal login/signup
|
|
19
29
|
* @param {any} param0
|
|
20
30
|
* @returns
|
|
21
31
|
*/
|
|
22
|
-
async function loginRequest({ email, password, isSignup,
|
|
23
|
-
|
|
32
|
+
async function loginRequest({ email, password, isSignup, code }) {
|
|
33
|
+
await refreshCsrfToken();
|
|
34
|
+
const opts = code
|
|
24
35
|
? {
|
|
25
|
-
method: "
|
|
26
|
-
path: "/auth/
|
|
36
|
+
method: "POST",
|
|
37
|
+
path: "/auth/session-from-code",
|
|
38
|
+
body: { code },
|
|
27
39
|
}
|
|
28
40
|
: isSignup
|
|
29
41
|
? {
|
|
30
42
|
method: "POST",
|
|
31
43
|
path: "/auth/signup",
|
|
32
|
-
body: {
|
|
33
|
-
email,
|
|
34
|
-
password,
|
|
35
|
-
},
|
|
44
|
+
body: { email, password },
|
|
36
45
|
}
|
|
37
46
|
: {
|
|
38
47
|
method: "POST",
|
|
39
|
-
path: "/auth/login-with/
|
|
40
|
-
body: {
|
|
41
|
-
email,
|
|
42
|
-
password,
|
|
43
|
-
},
|
|
48
|
+
path: "/auth/login-with/session",
|
|
49
|
+
body: { email, password },
|
|
44
50
|
};
|
|
45
51
|
const response = await apiCall(opts);
|
|
46
52
|
return response.data;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
|
-
* internal helper to process a
|
|
51
|
-
*
|
|
56
|
+
* internal helper to process a successful session login response
|
|
57
|
+
* ({ success: true, user }) and persist it locally
|
|
52
58
|
*/
|
|
53
|
-
const
|
|
54
|
-
const token = jwtDecode(tokenStr);
|
|
55
|
-
const user = token.user;
|
|
59
|
+
const handleSessionLogin = async (user, config) => {
|
|
56
60
|
config.user = user;
|
|
57
61
|
config.isPublicUser = false;
|
|
58
62
|
config.isOfflineMode = false;
|
|
63
|
+
config.hasSession = true;
|
|
59
64
|
await insertUser(user);
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
// login regenerated the session, so the previously fetched token is stale
|
|
66
|
+
await refreshCsrfToken();
|
|
67
|
+
await persistSession(config);
|
|
62
68
|
i18next.changeLanguage(user.language);
|
|
63
69
|
};
|
|
64
70
|
|
|
@@ -101,22 +107,22 @@ const getEntryPoint = (config) => {
|
|
|
101
107
|
};
|
|
102
108
|
|
|
103
109
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
110
|
+
* Normal login/signup use email and password. An OAuth-provider login
|
|
111
|
+
* (e.g. google-auth's finishLogin) instead passes the short-lived code
|
|
112
|
+
* its deep-link callback was handed, which stands in for a session here.
|
|
106
113
|
* @param {*} param0
|
|
107
114
|
*/
|
|
108
115
|
export async function login({ email, password, isSignup, token }) {
|
|
109
|
-
const loginResult =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (typeof loginResult === "string") {
|
|
116
|
+
const loginResult = await loginRequest({
|
|
117
|
+
email,
|
|
118
|
+
password,
|
|
119
|
+
isSignup,
|
|
120
|
+
code: token,
|
|
121
|
+
});
|
|
122
|
+
if (loginResult?.success && loginResult?.user) {
|
|
117
123
|
const alerts = [];
|
|
118
124
|
const config = saltcorn.data.state.getState().mobileConfig;
|
|
119
|
-
await
|
|
125
|
+
await handleSessionLogin(loginResult.user, config);
|
|
120
126
|
if (config.allowOfflineMode) alerts.push(await initialSync(config));
|
|
121
127
|
await tryInitPush(config);
|
|
122
128
|
await tryInitBackgroundSync(config);
|
|
@@ -129,14 +135,31 @@ export async function login({ email, password, isSignup, token }) {
|
|
|
129
135
|
});
|
|
130
136
|
|
|
131
137
|
// open first page
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
try {
|
|
139
|
+
const entryPoint = getEntryPoint(config);
|
|
140
|
+
addRoute({ route: entryPoint, query: undefined });
|
|
141
|
+
const page = await router.resolve({
|
|
142
|
+
pathname: entryPoint,
|
|
143
|
+
fullWrap: true,
|
|
144
|
+
alerts,
|
|
145
|
+
});
|
|
146
|
+
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Logged in fine, but this user can't open the app's entry point
|
|
149
|
+
// (e.g. its role doesn't allow it) - say so instead of leaving
|
|
150
|
+
// them stuck on the login page with no explanation.
|
|
151
|
+
showToasts([
|
|
152
|
+
{
|
|
153
|
+
type: "error",
|
|
154
|
+
msg: i18next.t("Logged in as %s, but unable to open the app: %s", {
|
|
155
|
+
postProcess: "sprintf",
|
|
156
|
+
sprintf: [config.user.email, error.message || "unknown error"],
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
} else if (loginResult?.error) {
|
|
162
|
+
showToasts([{ type: "error", msg: loginResult.error }]);
|
|
140
163
|
} else if (loginResult?.alerts) {
|
|
141
164
|
showToasts(loginResult?.alerts);
|
|
142
165
|
} else {
|
|
@@ -146,44 +169,39 @@ export async function login({ email, password, isSignup, token }) {
|
|
|
146
169
|
|
|
147
170
|
export async function publicLogin(entryPoint) {
|
|
148
171
|
try {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
} else if (loginResult?.alerts) {
|
|
183
|
-
showToasts(loginResult?.alerts);
|
|
184
|
-
} else {
|
|
185
|
-
throw new Error("The login failed.");
|
|
186
|
-
}
|
|
172
|
+
const config = saltcorn.data.state.getState().mobileConfig;
|
|
173
|
+
// no login needed for public, but a CSRF token is still cached upfront.
|
|
174
|
+
await refreshCsrfToken();
|
|
175
|
+
config.user = {
|
|
176
|
+
role_id: 100,
|
|
177
|
+
email: "public",
|
|
178
|
+
language: "en",
|
|
179
|
+
};
|
|
180
|
+
config.isPublicUser = true;
|
|
181
|
+
config.hasSession = true;
|
|
182
|
+
await persistSession(config);
|
|
183
|
+
i18next.changeLanguage(config.user.language);
|
|
184
|
+
addRoute({
|
|
185
|
+
route: entryPoint,
|
|
186
|
+
query: undefined,
|
|
187
|
+
});
|
|
188
|
+
const page = await router.resolve({
|
|
189
|
+
pathname: entryPoint,
|
|
190
|
+
fullWrap: true,
|
|
191
|
+
alerts: [
|
|
192
|
+
{
|
|
193
|
+
type: "success",
|
|
194
|
+
msg: i18next.t("Welcome to %s!", {
|
|
195
|
+
postProcess: "sprintf",
|
|
196
|
+
sprintf: [
|
|
197
|
+
saltcorn.data.state.getState().getConfig("site_name") ||
|
|
198
|
+
"Saltcorn",
|
|
199
|
+
],
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
187
205
|
} catch (error) {
|
|
188
206
|
console.error(error);
|
|
189
207
|
showToasts([
|
|
@@ -203,9 +221,11 @@ export async function logout() {
|
|
|
203
221
|
await tryStopBackgroundSync();
|
|
204
222
|
const response = await apiCall({ method: "GET", path: "/auth/logout" });
|
|
205
223
|
if (response.data.success) {
|
|
206
|
-
await
|
|
224
|
+
await clearSession();
|
|
207
225
|
clearHistory();
|
|
208
|
-
config.
|
|
226
|
+
config.csrfToken = undefined;
|
|
227
|
+
config.hasSession = false;
|
|
228
|
+
config.isPublicUser = false;
|
|
209
229
|
const page = await router.resolve({
|
|
210
230
|
pathname: "get/auth/login",
|
|
211
231
|
entryView: config.entry_point,
|
|
@@ -232,43 +252,34 @@ export async function insertUser({ id, email, role_id, language }) {
|
|
|
232
252
|
);
|
|
233
253
|
}
|
|
234
254
|
|
|
235
|
-
export async function
|
|
236
|
-
await saltcorn.data.db.deleteWhere("
|
|
255
|
+
export async function clearSession() {
|
|
256
|
+
await saltcorn.data.db.deleteWhere("session_table");
|
|
237
257
|
}
|
|
238
258
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
async function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const config = saltcorn.data.state.getState().mobileConfig;
|
|
252
|
-
await setJwt(response.data);
|
|
253
|
-
config.jwt = response.data;
|
|
254
|
-
}
|
|
255
|
-
} catch (e) {
|
|
256
|
-
console.error("JWT renewal failed:", e.message);
|
|
257
|
-
}
|
|
259
|
+
/**
|
|
260
|
+
* persists the current mobileConfig session state (csrf token, user,
|
|
261
|
+
* public/authenticated flag) so the app can restore it at next boot
|
|
262
|
+
* without a network call (used e.g. when starting offline).
|
|
263
|
+
*/
|
|
264
|
+
export async function persistSession(config) {
|
|
265
|
+
await clearSession();
|
|
266
|
+
await saltcorn.data.db.insert("session_table", {
|
|
267
|
+
csrfToken: config.csrfToken,
|
|
268
|
+
userJson: config.isPublicUser ? null : JSON.stringify(config.user),
|
|
269
|
+
isPublicUser: !!config.isPublicUser,
|
|
270
|
+
});
|
|
258
271
|
}
|
|
259
272
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return true;
|
|
273
|
-
} else return false;
|
|
273
|
+
/**
|
|
274
|
+
* checks with the server whether the current session cookie is still valid,
|
|
275
|
+
* and refreshes the cached CSRF token (its secret lives in the session, so
|
|
276
|
+
* it's fine to keep reusing the same token as long as the session is alive).
|
|
277
|
+
*/
|
|
278
|
+
export async function checkSession() {
|
|
279
|
+
const response = await apiCall({
|
|
280
|
+
method: "GET",
|
|
281
|
+
path: "/auth/authenticated",
|
|
282
|
+
timeout: 10000,
|
|
283
|
+
});
|
|
284
|
+
return !!response?.data?.authenticated;
|
|
274
285
|
}
|
|
@@ -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
|
+
}
|
package/src/helpers/common.js
CHANGED
|
@@ -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)
|
|
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 {
|
|
242
|
-
"../helpers/background_sync.js"
|
|
243
|
-
);
|
|
259
|
+
const { initBackground } = await import("../helpers/background.js");
|
|
244
260
|
try {
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
await initBackground(
|
|
262
|
+
config.syncInterval,
|
|
263
|
+
config.pushSyncHeartbeatInterval
|
|
264
|
+
);
|
|
247
265
|
} catch (error) {
|
|
248
|
-
console.error("Error initializing background
|
|
266
|
+
console.error("Error initializing background tasks:", error);
|
|
249
267
|
}
|
|
250
268
|
} catch (error) {
|
|
251
|
-
console.log("Background
|
|
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 {
|
|
279
|
-
"../helpers/background_sync.js"
|
|
280
|
-
);
|
|
296
|
+
const { stopBackground } = await import("../helpers/background.js");
|
|
281
297
|
try {
|
|
282
|
-
await
|
|
298
|
+
await stopBackground();
|
|
283
299
|
} catch (error) {
|
|
284
|
-
console.error("Error stopping
|
|
300
|
+
console.error("Error stopping background tasks:", error);
|
|
285
301
|
}
|
|
286
302
|
} catch (error) {
|
|
287
|
-
console.
|
|
303
|
+
console.log("Background module not available:", error);
|
|
288
304
|
}
|
|
289
305
|
}
|
package/src/helpers/db_schema.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*global saltcorn */
|
|
2
2
|
|
|
3
3
|
const historyFile = "update_history";
|
|
4
|
-
const
|
|
4
|
+
const sessionTableName = "session_table";
|
|
5
5
|
|
|
6
6
|
import { readFile, fileExists, writeJSON } from "./file_system";
|
|
7
7
|
import { Directory } from "@capacitor/filesystem";
|
|
@@ -206,8 +206,10 @@ export async function updateDb(tablesJSON) {
|
|
|
206
206
|
});
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
export async function
|
|
210
|
-
await saltcorn.data.db.query(`CREATE TABLE IF NOT EXISTS ${
|
|
211
|
-
|
|
209
|
+
export async function createSessionTable() {
|
|
210
|
+
await saltcorn.data.db.query(`CREATE TABLE IF NOT EXISTS ${sessionTableName} (
|
|
211
|
+
csrfToken VARCHAR(500),
|
|
212
|
+
userJson VARCHAR(2000),
|
|
213
|
+
isPublicUser BOOLEAN
|
|
212
214
|
)`);
|
|
213
215
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Filesystem, Encoding, Directory } from "@capacitor/filesystem";
|
|
2
|
+
// side-effect only: registers the plugin so it works on the web platform too,
|
|
3
|
+
// same as Filesystem above (native platforms pick it up either way)
|
|
4
|
+
import "@capacitor/file-transfer";
|
|
2
5
|
|
|
3
6
|
export async function writeFile(name, directory, content) {
|
|
4
7
|
try {
|
|
@@ -59,11 +59,11 @@ const insertRemoteData = async (table, rows, syncTimestamp) => {
|
|
|
59
59
|
const ref = _sync_info_tbl_ref_,
|
|
60
60
|
last_modified = _sync_info_tbl_last_modified_;
|
|
61
61
|
await saltcorn.data.db.insert(tblName, rest, { replace: true });
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
// ref is inlined as a literal, not passed as a param, so numeric-looking
|
|
63
|
+
// refs like "19" don't get silently turned into "19.0" on iOS.
|
|
64
|
+
const { rows: infos } = await saltcorn.data.db.query(
|
|
65
|
+
`select 1 from "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
|
|
66
|
+
where ref=${sqlRef(rest[pkName])}`
|
|
67
67
|
);
|
|
68
68
|
if (infos.length > 0) {
|
|
69
69
|
await saltcorn.data.db.query(
|
|
@@ -72,14 +72,10 @@ const insertRemoteData = async (table, rows, syncTimestamp) => {
|
|
|
72
72
|
where ref=${sqlRef(rest[pkName])}`
|
|
73
73
|
);
|
|
74
74
|
} else {
|
|
75
|
-
await saltcorn.data.db.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
last_modified: syncTimestamp,
|
|
80
|
-
deleted: false,
|
|
81
|
-
modified_local: false,
|
|
82
|
-
}
|
|
75
|
+
await saltcorn.data.db.query(
|
|
76
|
+
`insert into "${saltcorn.data.db.sqlsanitize(tblName)}_sync_info"
|
|
77
|
+
(ref, last_modified, deleted, modified_local)
|
|
78
|
+
values (${sqlRef(ref)}, ${syncTimestamp}, false, false)`
|
|
83
79
|
);
|
|
84
80
|
}
|
|
85
81
|
}
|