@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/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { init } from "./init";
|
|
1
|
+
import { init, postShare } 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";
|
|
@@ -12,7 +12,7 @@ import { router } from "./routing/index";
|
|
|
12
12
|
let notifications = undefined;
|
|
13
13
|
try {
|
|
14
14
|
notifications = require("./helpers/notifications");
|
|
15
|
-
console.log("Notifications module available");
|
|
15
|
+
console.log("Notifications module available");
|
|
16
16
|
} catch (err) {
|
|
17
17
|
console.log("Notifications module not available");
|
|
18
18
|
}
|
|
@@ -21,9 +21,8 @@ try {
|
|
|
21
21
|
let backgroundSync = undefined;
|
|
22
22
|
try {
|
|
23
23
|
backgroundSync = require("./helpers/background_sync");
|
|
24
|
-
console.log("Background sync module available");
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
24
|
+
console.log("Background sync module available");
|
|
25
|
+
} catch (err) {
|
|
27
26
|
console.log("Background sync module not available");
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -38,6 +37,7 @@ context.keys().forEach((key) => {
|
|
|
38
37
|
|
|
39
38
|
export const mobileApp = {
|
|
40
39
|
init,
|
|
40
|
+
postShare,
|
|
41
41
|
api,
|
|
42
42
|
auth,
|
|
43
43
|
common,
|
package/src/init.js
CHANGED
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
createSyncInfoTables,
|
|
12
12
|
dbUpdateNeeded,
|
|
13
13
|
updateDb,
|
|
14
|
-
|
|
14
|
+
createSessionTable,
|
|
15
15
|
} from "./helpers/db_schema.js";
|
|
16
|
-
import { publicLogin,
|
|
16
|
+
import { publicLogin, checkSession } from "./helpers/auth.js";
|
|
17
17
|
import { router } from "./routing/index.js";
|
|
18
18
|
import {
|
|
19
19
|
replaceIframe,
|
|
@@ -30,7 +30,6 @@ import { readJSONCordova, readTextCordova } from "./helpers/file_system.js";
|
|
|
30
30
|
|
|
31
31
|
import i18next from "i18next";
|
|
32
32
|
import i18nextSprintfPostProcessor from "i18next-sprintf-postprocessor";
|
|
33
|
-
import { jwtDecode } from "jwt-decode";
|
|
34
33
|
|
|
35
34
|
import { Network } from "@capacitor/network";
|
|
36
35
|
import { App } from "@capacitor/app";
|
|
@@ -152,19 +151,34 @@ const handlePluginHeaders = async (plugins) => {
|
|
|
152
151
|
}
|
|
153
152
|
};
|
|
154
153
|
|
|
155
|
-
const
|
|
156
|
-
const rows = await saltcorn.data.db.select("
|
|
157
|
-
return rows?.length > 0 ? rows[0]
|
|
154
|
+
const getSession = async () => {
|
|
155
|
+
const rows = await saltcorn.data.db.select("session_table");
|
|
156
|
+
return rows?.length > 0 ? rows[0] : null;
|
|
158
157
|
};
|
|
159
158
|
|
|
160
|
-
const
|
|
161
|
-
if (!(await saltcorn.data.db.tableExists("
|
|
162
|
-
await
|
|
159
|
+
const initSession = async () => {
|
|
160
|
+
if (!(await saltcorn.data.db.tableExists("session_table"))) {
|
|
161
|
+
await createSessionTable();
|
|
163
162
|
} else {
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
163
|
+
const row = await getSession();
|
|
164
|
+
if (row?.csrfToken) {
|
|
166
165
|
const state = saltcorn.data.state.getState();
|
|
167
|
-
state.mobileConfig.
|
|
166
|
+
state.mobileConfig.csrfToken = row.csrfToken;
|
|
167
|
+
state.mobileConfig.isPublicUser = !!row.isPublicUser;
|
|
168
|
+
state.mobileConfig.hasSession = true;
|
|
169
|
+
if (row.isPublicUser) {
|
|
170
|
+
state.mobileConfig.user = {
|
|
171
|
+
role_id: 100,
|
|
172
|
+
email: "public",
|
|
173
|
+
language: "en",
|
|
174
|
+
};
|
|
175
|
+
} else if (row.userJson) {
|
|
176
|
+
try {
|
|
177
|
+
state.mobileConfig.user = JSON.parse(row.userJson);
|
|
178
|
+
} catch {
|
|
179
|
+
// corrupted local row, fall back to a network check
|
|
180
|
+
}
|
|
181
|
+
}
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
};
|
|
@@ -210,7 +224,7 @@ const showErrorPage = async (error) => {
|
|
|
210
224
|
const onResume = async () => {
|
|
211
225
|
if (typeof saltcorn === "undefined") return;
|
|
212
226
|
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
213
|
-
if (mobileConfig?.
|
|
227
|
+
if (mobileConfig?.hasSession) {
|
|
214
228
|
const netStatus = await Network.getStatus();
|
|
215
229
|
mobileConfig.networkState = netStatus.connectionType;
|
|
216
230
|
if (
|
|
@@ -226,8 +240,8 @@ const onResume = async () => {
|
|
|
226
240
|
await showErrorPage(error);
|
|
227
241
|
}
|
|
228
242
|
} else if (mobileConfig.networkState !== "none") {
|
|
229
|
-
if (!mobileConfig.isOfflineMode && !
|
|
230
|
-
await
|
|
243
|
+
if (!mobileConfig.isOfflineMode && !mobileConfig.isPublicUser) {
|
|
244
|
+
await checkSession();
|
|
231
245
|
}
|
|
232
246
|
if (mobileConfig.allowOfflineMode && mobileConfig.syncOnAppResume) {
|
|
233
247
|
try {
|
|
@@ -240,21 +254,6 @@ const onResume = async () => {
|
|
|
240
254
|
}
|
|
241
255
|
};
|
|
242
256
|
|
|
243
|
-
const isPublicJwt = (jwt) => {
|
|
244
|
-
try {
|
|
245
|
-
if (!jwt) return false;
|
|
246
|
-
const decoded = jwtDecode(jwt);
|
|
247
|
-
return decoded.sub === "public";
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.log(
|
|
250
|
-
`Unable to inspect '${jwt}': ${
|
|
251
|
-
error.message ? error.message : "Unknown error"
|
|
252
|
-
}`
|
|
253
|
-
);
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
|
|
258
257
|
const isPublicEntryPoint = async (entryPoint) => {
|
|
259
258
|
if (!entryPoint) return false;
|
|
260
259
|
try {
|
|
@@ -309,7 +308,7 @@ const notEmpty = (shareData) => {
|
|
|
309
308
|
}
|
|
310
309
|
};
|
|
311
310
|
|
|
312
|
-
const postShare = async (shareData) => {
|
|
311
|
+
export const postShare = async (shareData) => {
|
|
313
312
|
const page = await router.resolve({
|
|
314
313
|
pathname: "post/notifications/share",
|
|
315
314
|
shareData,
|
|
@@ -390,7 +389,7 @@ export async function init(mobileConfig) {
|
|
|
390
389
|
try {
|
|
391
390
|
const url = event.url;
|
|
392
391
|
if (url.startsWith("mobileapp://auth/callback")) {
|
|
393
|
-
const token = new URL(url).searchParams.get("
|
|
392
|
+
const token = new URL(url).searchParams.get("code");
|
|
394
393
|
const method = new URL(url).searchParams.get("method");
|
|
395
394
|
const methods = saltcorn.data.state.getState().auth_methods;
|
|
396
395
|
if (!methods[method])
|
|
@@ -429,7 +428,7 @@ export async function init(mobileConfig) {
|
|
|
429
428
|
await updateDb(tablesJSON);
|
|
430
429
|
}
|
|
431
430
|
await createSyncInfoTables(mobileConfig.synchedTables);
|
|
432
|
-
await
|
|
431
|
+
await initSession();
|
|
433
432
|
await state.refresh_tables();
|
|
434
433
|
await state.refresh_views();
|
|
435
434
|
await state.refresh_pages();
|
|
@@ -455,14 +454,16 @@ export async function init(mobileConfig) {
|
|
|
455
454
|
Network.addListener("networkStatusChange", networkChangeCallback);
|
|
456
455
|
|
|
457
456
|
const networkDisabled = state.mobileConfig.networkState === "none";
|
|
458
|
-
const
|
|
457
|
+
const hasSession = !!state.mobileConfig.hasSession;
|
|
458
|
+
const isPublicUser = !!state.mobileConfig.isPublicUser;
|
|
459
459
|
const alerts = [];
|
|
460
|
-
if (
|
|
461
|
-
|
|
460
|
+
if (
|
|
461
|
+
hasSession &&
|
|
462
|
+
!isPublicUser &&
|
|
463
|
+
(networkDisabled || (await checkSession()))
|
|
464
|
+
) {
|
|
465
|
+
// already logged in, continue - mobileConfig.user was restored by initSession()
|
|
462
466
|
const mobileConfig = state.mobileConfig;
|
|
463
|
-
const decodedJwt = jwtDecode(mobileConfig.jwt);
|
|
464
|
-
mobileConfig.user = decodedJwt.user;
|
|
465
|
-
mobileConfig.isPublicUser = false;
|
|
466
467
|
await i18next.changeLanguage(mobileConfig.user.language);
|
|
467
468
|
if (mobileConfig.allowOfflineMode) {
|
|
468
469
|
const { offlineUser } = (await getLastOfflineSession()) || {};
|
|
@@ -536,11 +537,9 @@ export async function init(mobileConfig) {
|
|
|
536
537
|
});
|
|
537
538
|
}
|
|
538
539
|
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
539
|
-
} else if (
|
|
540
|
+
} else if (hasSession && isPublicUser) {
|
|
540
541
|
// already logged in as public
|
|
541
542
|
const config = state.mobileConfig;
|
|
542
|
-
config.user = { role_id: 100, email: "public", language: "en" };
|
|
543
|
-
config.isPublicUser = true;
|
|
544
543
|
i18next.changeLanguage(config.user.language);
|
|
545
544
|
const entryPoint = getEntryPoint(100, state, state.mobileConfig);
|
|
546
545
|
if (!entryPoint) await showLogin(alerts);
|
|
@@ -29,7 +29,7 @@ export function MobileRequest({
|
|
|
29
29
|
}),
|
|
30
30
|
isAuthenticated: () => {
|
|
31
31
|
const mobileCfg = saltcorn.data.state.getState().mobileConfig;
|
|
32
|
-
return mobileCfg && mobileCfg.
|
|
32
|
+
return mobileCfg && mobileCfg.hasSession && !mobileCfg.isPublicUser;
|
|
33
33
|
},
|
|
34
34
|
getLocale: () => {
|
|
35
35
|
const mobileCfg = saltcorn.data.state.getState().mobileConfig;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { wrapContents } from "../utils";
|
|
4
4
|
import { MobileRequest } from "../mocks/request";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { checkSession } from "../../helpers/auth.js";
|
|
7
7
|
import { apiCall } from "../../helpers/api";
|
|
8
8
|
|
|
9
9
|
const buildErrMsg = (msg) => {
|
|
@@ -24,21 +24,45 @@ const buildSuccessMsg = (msg) => {
|
|
|
24
24
|
);
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// files have a real type (e.g. image/png); text/link shares are "text/plain"
|
|
28
|
+
const isFileItem = (item) =>
|
|
29
|
+
item?.type && item.type !== "text/plain" && item.url;
|
|
30
|
+
|
|
27
31
|
export const postShare = async (context) => {
|
|
28
32
|
let content = "";
|
|
29
33
|
const mobileCfg = saltcorn.data.state.getState().mobileConfig;
|
|
30
34
|
if (mobileCfg.networkState === "none")
|
|
31
35
|
content = buildErrMsg("No network connection");
|
|
32
|
-
else if (!(await
|
|
33
|
-
content = buildErrMsg("Not authenticated");
|
|
36
|
+
else if (!(await checkSession())) content = buildErrMsg("Not authenticated");
|
|
34
37
|
else {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
body
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
try {
|
|
39
|
+
const { title, description, type, url, additionalItems } =
|
|
40
|
+
context.shareData || {};
|
|
41
|
+
let body = context.shareData;
|
|
42
|
+
if (isFileItem({ type, url })) {
|
|
43
|
+
const items = [{ title, type, url }, ...(additionalItems || [])];
|
|
44
|
+
// title is the real on-disk filename, location is where to find it
|
|
45
|
+
const files = [];
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
const uploaded = await saltcorn.data.models.File.uploadFromPath(
|
|
48
|
+
item.url,
|
|
49
|
+
item.title || "shared-file",
|
|
50
|
+
item.type
|
|
51
|
+
);
|
|
52
|
+
files.push({ title: uploaded.filename, location: uploaded.location });
|
|
53
|
+
}
|
|
54
|
+
body = { description, files };
|
|
55
|
+
}
|
|
56
|
+
const response = await apiCall({
|
|
57
|
+
method: "POST",
|
|
58
|
+
path: "/notifications/share-handler",
|
|
59
|
+
body,
|
|
60
|
+
});
|
|
61
|
+
if (response.error) content = buildErrMsg(response.error);
|
|
62
|
+
else content = buildSuccessMsg("Shared successfully");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
content = buildErrMsg(error.message || "Error sharing");
|
|
65
|
+
}
|
|
42
66
|
}
|
|
43
67
|
return await wrapContents(
|
|
44
68
|
saltcorn.markup.tags.div(
|
|
@@ -39,7 +39,7 @@ export const postView = async (context) => {
|
|
|
39
39
|
const mobileCfg = state.mobileConfig;
|
|
40
40
|
if (
|
|
41
41
|
mobileCfg.user.role_id > view.min_role &&
|
|
42
|
-
!(await view.
|
|
42
|
+
!(await view.authorize(mobileCfg.user, { action: "post", req, body }))
|
|
43
43
|
) {
|
|
44
44
|
throw new saltcorn.data.utils.NotAuthorized(req.__("Not authorized"));
|
|
45
45
|
}
|
|
@@ -150,7 +150,11 @@ export const getView = async (context) => {
|
|
|
150
150
|
if (!view) throw new Error(req.__("No such view: %s", viewname));
|
|
151
151
|
if (
|
|
152
152
|
state.mobileConfig.user.role_id > view.min_role &&
|
|
153
|
-
!(await view.
|
|
153
|
+
!(await view.authorize(state.mobileConfig.user, {
|
|
154
|
+
action: "get",
|
|
155
|
+
req,
|
|
156
|
+
state: query,
|
|
157
|
+
}))
|
|
154
158
|
) {
|
|
155
159
|
const additionalInfos = `: your role: ${state.mobileConfig.user.role_id}, view min_role: ${view.min_role}`;
|
|
156
160
|
throw new saltcorn.data.utils.NotAuthorized(
|
|
@@ -161,7 +165,9 @@ export const getView = async (context) => {
|
|
|
161
165
|
if (!view.renderLocally()) {
|
|
162
166
|
const response = await apiCall({
|
|
163
167
|
method: "GET",
|
|
164
|
-
path: `/view/${encodeURIComponent(viewname)}${
|
|
168
|
+
path: `/view/${encodeURIComponent(viewname)}${
|
|
169
|
+
context.query ? `?${context.query}` : ""
|
|
170
|
+
}`,
|
|
165
171
|
});
|
|
166
172
|
const data = response.data;
|
|
167
173
|
contents0 = data;
|
package/src/routing/utils.js
CHANGED
|
@@ -24,7 +24,7 @@ export const getHeaders = () => {
|
|
|
24
24
|
from_cfg.push({ style: state.getConfig("page_custom_css", "") });
|
|
25
25
|
if (state.getConfig("page_custom_html", ""))
|
|
26
26
|
from_cfg.push({ headerTag: state.getConfig("page_custom_html", "") });
|
|
27
|
-
return [...stdHeaders, ...config.pluginHeaders, ...from_cfg];
|
|
27
|
+
return [...stdHeaders, ...(config.pluginHeaders || []), ...from_cfg];
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export const parseQuery = (queryStr) => {
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
/*eslint-env browser*/
|
|
2
2
|
/*global $, KTDrawer, reload_embedded_view, submitWithEmptyAction, is_paging_param, bootstrap, common_done, unique_field_from_rows, inline_submit_success, get_current_state_url, initialize_page, reset_spinners */
|
|
3
3
|
|
|
4
|
+
// Tapping a javascript: link silently does nothing on iOS, so we run the
|
|
5
|
+
// code ourselves instead of relying on the link to do it.
|
|
6
|
+
document.addEventListener("click", function (event) {
|
|
7
|
+
const link =
|
|
8
|
+
event.target.closest && event.target.closest("a[href^='javascript:']");
|
|
9
|
+
if (!link) return;
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
const code = link.getAttribute("href").slice("javascript:".length);
|
|
12
|
+
try {
|
|
13
|
+
(0, eval)(code);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("Error executing javascript: link:", error);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
4
19
|
function combineFormAndQuery(form, query) {
|
|
5
20
|
let paramsList = [];
|
|
6
21
|
const formData = new FormData(form[0]);
|
|
@@ -87,7 +102,10 @@ async function formSubmit(e, urlSuffix, viewname, noSubmitCb, matchingState) {
|
|
|
87
102
|
const urlParams = new URLSearchParams();
|
|
88
103
|
const data = matchingState ? {} : null;
|
|
89
104
|
for (const entry of new FormData(e).entries()) {
|
|
90
|
-
|
|
105
|
+
// cordova-plugin-file replaces window.File with its own class, so a
|
|
106
|
+
// real picked file no longer passes "instanceof File" - Blob still
|
|
107
|
+
// works, since every File is also a Blob.
|
|
108
|
+
if (entry[1] instanceof Blob) files[entry[0]] = entry[1];
|
|
91
109
|
else {
|
|
92
110
|
// is there a hidden input with a filename?
|
|
93
111
|
const domEl = $(e).find(
|
|
@@ -431,22 +449,50 @@ async function pjax_to(href, query, e) {
|
|
|
431
449
|
await parent.saltcorn.mobileApp.navigation.handleRoute(safeHref, query);
|
|
432
450
|
else
|
|
433
451
|
try {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
452
|
+
const { isOfflineMode } =
|
|
453
|
+
parent.saltcorn.data.state.getState().mobileConfig;
|
|
454
|
+
let content;
|
|
455
|
+
if (isOfflineMode) {
|
|
456
|
+
const page = await parent.saltcorn.mobileApp.navigation.router.resolve(
|
|
457
|
+
{
|
|
458
|
+
pathname: `get${safeHref}`,
|
|
459
|
+
query: query,
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
if (page.redirect) {
|
|
463
|
+
const { path: redirectPath, query: redirectQuery } =
|
|
464
|
+
parent.saltcorn.mobileApp.navigation.splitPathQuery(
|
|
465
|
+
page.redirect
|
|
466
|
+
);
|
|
467
|
+
await parent.saltcorn.mobileApp.navigation.handleRoute(
|
|
468
|
+
redirectPath.startsWith("/") && redirectPath.length > 1
|
|
469
|
+
? `get${redirectPath}`
|
|
470
|
+
: redirectPath,
|
|
471
|
+
redirectQuery
|
|
472
|
+
);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
content = typeof page === "string" ? page : page.content;
|
|
476
|
+
} else {
|
|
477
|
+
const headers = {
|
|
478
|
+
pjaxpageload: "true",
|
|
479
|
+
};
|
|
480
|
+
if (localizer.length) headers.localizedstate = "true";
|
|
481
|
+
const result = await parent.saltcorn.mobileApp.api.apiCall({
|
|
482
|
+
path: path,
|
|
483
|
+
method: "GET",
|
|
484
|
+
additionalHeaders: headers,
|
|
485
|
+
responseType: "text",
|
|
486
|
+
});
|
|
487
|
+
content = result.data;
|
|
488
|
+
}
|
|
443
489
|
if (!inModal && !localizer.length) {
|
|
444
490
|
// not sure for mobile
|
|
445
491
|
// window.history.pushState({ url: href }, "", href);
|
|
446
492
|
}
|
|
447
493
|
if (inModal && !localizer.length)
|
|
448
494
|
$(".sc-modal-linkout").attr("href", path);
|
|
449
|
-
$dest.html(
|
|
495
|
+
$dest.html(content);
|
|
450
496
|
if (localizer.length) localizer.attr("data-sc-local-state", path);
|
|
451
497
|
initialize_page();
|
|
452
498
|
} catch (error) {
|
|
@@ -675,17 +721,16 @@ async function make_unique_field(
|
|
|
675
721
|
}
|
|
676
722
|
}
|
|
677
723
|
|
|
678
|
-
function openFile(fileId) {
|
|
724
|
+
async function openFile(fileId) {
|
|
679
725
|
const config = parent.saltcorn.data.state.getState().mobileConfig;
|
|
680
726
|
const serverPath = config.server_path;
|
|
681
|
-
const
|
|
682
|
-
const url = `${serverPath}/files/serve/${encodeURIComponent(
|
|
683
|
-
fileId
|
|
684
|
-
)}?jwt=${token}`;
|
|
727
|
+
const url = `${serverPath}/files/serve/${encodeURIComponent(fileId)}`;
|
|
685
728
|
parent.cordova.InAppBrowser.open(
|
|
686
729
|
url,
|
|
687
730
|
"_blank",
|
|
688
|
-
|
|
731
|
+
// No clearcache - it clears cookies app-wide (Android's cookie store
|
|
732
|
+
// isn't scoped per tab), which would log the main app out too.
|
|
733
|
+
"location=no,toolbar=yes,toolbarposition=top"
|
|
689
734
|
);
|
|
690
735
|
}
|
|
691
736
|
|
|
@@ -695,7 +740,9 @@ function openInAppBrowser(url, domId) {
|
|
|
695
740
|
const ref = parent.cordova.InAppBrowser.open(
|
|
696
741
|
url,
|
|
697
742
|
"_blank",
|
|
698
|
-
|
|
743
|
+
// No clearcache - it clears cookies app-wide (Android's cookie store
|
|
744
|
+
// isn't scoped per tab), which would log the main app out too.
|
|
745
|
+
"location=no,toolbar=yes,toolbarposition=top"
|
|
699
746
|
);
|
|
700
747
|
ref.addEventListener("exit", function () {
|
|
701
748
|
$(`#${domId}`).find(".spinner-border").addClass("d-none");
|
package/add_entitlements.rb
DELETED
|
@@ -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
|
-
}
|