@saltcorn/mobile-app 1.5.0-beta.0 → 1.5.0-beta.10
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/package.json +1 -1
- package/src/helpers/auth.js +70 -57
- package/src/helpers/db_schema.js +1 -1
- package/src/helpers/notifications.js +6 -0
- package/src/index.js +2 -1
- package/src/init.js +30 -19
- package/src/routing/routes/auth.js +28 -1
- package/src/routing/routes/notifications.js +1 -1
- package/src/routing/utils.js +3 -1
- package/www/index.html +1 -1
- package/www/js/iframe_view_utils.js +22 -2
package/package.json
CHANGED
package/src/helpers/auth.js
CHANGED
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
unregisterPushNotifications,
|
|
13
13
|
} from "../helpers/notifications";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* internal helper for the normal login/signup and public login
|
|
17
|
+
* @param {any} param0
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
15
20
|
async function loginRequest({ email, password, isSignup, isPublic }) {
|
|
16
21
|
const opts = isPublic
|
|
17
22
|
? {
|
|
@@ -39,6 +44,70 @@ async function loginRequest({ email, password, isSignup, isPublic }) {
|
|
|
39
44
|
return response.data;
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
/**
|
|
48
|
+
* helper for normal logins and auth provider logins
|
|
49
|
+
* @param {string} token
|
|
50
|
+
*/
|
|
51
|
+
export async function handleToken(token) {
|
|
52
|
+
const decodedJwt = jwtDecode(token);
|
|
53
|
+
const state = saltcorn.data.state.getState();
|
|
54
|
+
const config = state.mobileConfig;
|
|
55
|
+
config.user = decodedJwt.user;
|
|
56
|
+
config.isPublicUser = false;
|
|
57
|
+
config.isOfflineMode = false;
|
|
58
|
+
await insertUser(config.user);
|
|
59
|
+
await setJwt(token);
|
|
60
|
+
config.jwt = token;
|
|
61
|
+
i18next.changeLanguage(config.user.language);
|
|
62
|
+
const alerts = [];
|
|
63
|
+
if (config.allowOfflineMode) {
|
|
64
|
+
const { offlineUser, hasOfflineData } =
|
|
65
|
+
(await getLastOfflineSession()) || {};
|
|
66
|
+
if (!offlineUser || offlineUser === config.user.email) {
|
|
67
|
+
await sync();
|
|
68
|
+
} else {
|
|
69
|
+
if (hasOfflineData)
|
|
70
|
+
alerts.push({
|
|
71
|
+
type: "warning",
|
|
72
|
+
msg: `'${offlineUser}' has not yet uploaded offline data.`,
|
|
73
|
+
});
|
|
74
|
+
else {
|
|
75
|
+
await deleteOfflineData(true);
|
|
76
|
+
await sync();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (saltcorn.data.utils.isPushEnabled(config.user)) {
|
|
81
|
+
initPushNotifications();
|
|
82
|
+
} else {
|
|
83
|
+
await unregisterPushNotifications();
|
|
84
|
+
}
|
|
85
|
+
alerts.push({
|
|
86
|
+
type: "success",
|
|
87
|
+
msg: i18next.t("Welcome, %s!", {
|
|
88
|
+
postProcess: "sprintf",
|
|
89
|
+
sprintf: [config.user.email],
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
let entryPoint = null;
|
|
94
|
+
if (config.entryPointType === "byrole") {
|
|
95
|
+
const homepageByRole = state.getConfig("home_page_by_role", {})[
|
|
96
|
+
config.user.role_id
|
|
97
|
+
];
|
|
98
|
+
if (homepageByRole) entryPoint = `get/page/${homepageByRole}`;
|
|
99
|
+
else throw new Error("No homepage defined for this role.");
|
|
100
|
+
} else entryPoint = config.entry_point;
|
|
101
|
+
|
|
102
|
+
addRoute({ route: entryPoint, query: undefined });
|
|
103
|
+
const page = await router.resolve({
|
|
104
|
+
pathname: entryPoint,
|
|
105
|
+
fullWrap: true,
|
|
106
|
+
alerts,
|
|
107
|
+
});
|
|
108
|
+
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
109
|
+
}
|
|
110
|
+
|
|
42
111
|
export async function login({ email, password, isSignup }) {
|
|
43
112
|
const loginResult = await loginRequest({
|
|
44
113
|
email,
|
|
@@ -47,63 +116,7 @@ export async function login({ email, password, isSignup }) {
|
|
|
47
116
|
});
|
|
48
117
|
if (typeof loginResult === "string") {
|
|
49
118
|
// use it as a token
|
|
50
|
-
|
|
51
|
-
const state = saltcorn.data.state.getState();
|
|
52
|
-
const config = state.mobileConfig;
|
|
53
|
-
config.user = decodedJwt.user;
|
|
54
|
-
config.isPublicUser = false;
|
|
55
|
-
config.isOfflineMode = false;
|
|
56
|
-
await insertUser(config.user);
|
|
57
|
-
await setJwt(loginResult);
|
|
58
|
-
config.jwt = loginResult;
|
|
59
|
-
i18next.changeLanguage(config.user.language);
|
|
60
|
-
const alerts = [];
|
|
61
|
-
if (config.allowOfflineMode) {
|
|
62
|
-
const { offlineUser, hasOfflineData } =
|
|
63
|
-
(await getLastOfflineSession()) || {};
|
|
64
|
-
if (!offlineUser || offlineUser === config.user.email) {
|
|
65
|
-
await sync();
|
|
66
|
-
} else {
|
|
67
|
-
if (hasOfflineData)
|
|
68
|
-
alerts.push({
|
|
69
|
-
type: "warning",
|
|
70
|
-
msg: `'${offlineUser}' has not yet uploaded offline data.`,
|
|
71
|
-
});
|
|
72
|
-
else {
|
|
73
|
-
await deleteOfflineData(true);
|
|
74
|
-
await sync();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (saltcorn.data.utils.isPushEnabled(config.user)) {
|
|
79
|
-
initPushNotifications();
|
|
80
|
-
} else {
|
|
81
|
-
await unregisterPushNotifications();
|
|
82
|
-
}
|
|
83
|
-
alerts.push({
|
|
84
|
-
type: "success",
|
|
85
|
-
msg: i18next.t("Welcome, %s!", {
|
|
86
|
-
postProcess: "sprintf",
|
|
87
|
-
sprintf: [config.user.email],
|
|
88
|
-
}),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
let entryPoint = null;
|
|
92
|
-
if (config.entryPointType === "byrole") {
|
|
93
|
-
const homepageByRole = state.getConfig("home_page_by_role", {})[
|
|
94
|
-
config.user.role_id
|
|
95
|
-
];
|
|
96
|
-
if (homepageByRole) entryPoint = `get/page/${homepageByRole}`;
|
|
97
|
-
else throw new Error("No homepage defined for this role.");
|
|
98
|
-
} else entryPoint = config.entry_point;
|
|
99
|
-
|
|
100
|
-
addRoute({ route: entryPoint, query: undefined });
|
|
101
|
-
const page = await router.resolve({
|
|
102
|
-
pathname: entryPoint,
|
|
103
|
-
fullWrap: true,
|
|
104
|
-
alerts,
|
|
105
|
-
});
|
|
106
|
-
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
119
|
+
await handleToken(loginResult);
|
|
107
120
|
} else if (loginResult?.alerts) {
|
|
108
121
|
showAlerts(loginResult?.alerts);
|
|
109
122
|
} else {
|
package/src/helpers/db_schema.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function updateScPlugins(tablesJSON) {
|
|
|
78
78
|
|
|
79
79
|
export async function updateUserDefinedTables() {
|
|
80
80
|
const existingTables = await saltcorn.data.db.listUserDefinedTables();
|
|
81
|
-
const tables = await saltcorn.data.models.Table.find();
|
|
81
|
+
const tables = await saltcorn.data.models.Table.find({}, { cached: true });
|
|
82
82
|
for (const table of tables) {
|
|
83
83
|
const sanitized = saltcorn.data.db.sqlsanitize(table.name);
|
|
84
84
|
if (
|
|
@@ -2,6 +2,9 @@ import { Capacitor } from "@capacitor/core";
|
|
|
2
2
|
import { apiCall } from "./api";
|
|
3
3
|
import { showAlerts } from "./common";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @capacitor/push-notifications isn't always included in the build
|
|
7
|
+
*/
|
|
5
8
|
async function loadNotificationsPlugin() {
|
|
6
9
|
try {
|
|
7
10
|
const { PushNotifications } = await import("@capacitor/push-notifications");
|
|
@@ -12,6 +15,9 @@ async function loadNotificationsPlugin() {
|
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @capacitor/device isn't always included in the build
|
|
20
|
+
*/
|
|
15
21
|
async function loadDevicePlugin() {
|
|
16
22
|
try {
|
|
17
23
|
const { Device } = await import("@capacitor/device");
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,8 @@ import { router } from "./routing/index";
|
|
|
12
12
|
const plugins = {};
|
|
13
13
|
const context = require.context("./plugins-code", true, /index\.js$/);
|
|
14
14
|
context.keys().forEach((key) => {
|
|
15
|
-
const
|
|
15
|
+
const tokens = key.split("/");
|
|
16
|
+
const pluginName = tokens[tokens.length - 2];
|
|
16
17
|
plugins[pluginName] = context(key);
|
|
17
18
|
});
|
|
18
19
|
|
package/src/init.js
CHANGED
|
@@ -171,7 +171,7 @@ const initI18Next = async () => {
|
|
|
171
171
|
for (const key of Object.keys(
|
|
172
172
|
saltcorn.data.models.config.available_languages
|
|
173
173
|
)) {
|
|
174
|
-
if (Capacitor.
|
|
174
|
+
if (Capacitor.getPlatform() !== "web") {
|
|
175
175
|
const localeFile = await readJSONCordova(
|
|
176
176
|
`${key}.json`,
|
|
177
177
|
`${cordova.file.applicationDirectory}public/data/locales`
|
|
@@ -219,17 +219,6 @@ const onResume = async () => {
|
|
|
219
219
|
await startOfflineMode();
|
|
220
220
|
clearHistory();
|
|
221
221
|
if (mobileConfig.user?.id) await gotoEntryView();
|
|
222
|
-
else {
|
|
223
|
-
const decodedJwt = jwtDecode(mobileConfig.jwt);
|
|
224
|
-
mobileConfig.user = decodedJwt.user;
|
|
225
|
-
mobileConfig.isPublicUser = false;
|
|
226
|
-
}
|
|
227
|
-
addRoute({ route: mobileConfig.entry_point, query: undefined });
|
|
228
|
-
const page = await router.resolve({
|
|
229
|
-
pathname: mobileConfig.entry_point,
|
|
230
|
-
fullWrap: true,
|
|
231
|
-
alerts: [],
|
|
232
|
-
});
|
|
233
222
|
} catch (error) {
|
|
234
223
|
await showErrorPage(error);
|
|
235
224
|
}
|
|
@@ -317,7 +306,7 @@ const postShare = async (shareData) => {
|
|
|
317
306
|
};
|
|
318
307
|
|
|
319
308
|
const readSchemaIfNeeded = async () => {
|
|
320
|
-
if (Capacitor.
|
|
309
|
+
if (Capacitor.getPlatform() !== "web") {
|
|
321
310
|
let tablesJSON = null;
|
|
322
311
|
const { created_at } = await readJSONCordova(
|
|
323
312
|
"tables_created_at.json",
|
|
@@ -337,8 +326,8 @@ const readSchemaIfNeeded = async () => {
|
|
|
337
326
|
}
|
|
338
327
|
};
|
|
339
328
|
|
|
340
|
-
const readSiteLogo = async (
|
|
341
|
-
if (Capacitor.
|
|
329
|
+
const readSiteLogo = async () => {
|
|
330
|
+
if (Capacitor.getPlatform() === "web") return "";
|
|
342
331
|
try {
|
|
343
332
|
const base64 = await readTextCordova(
|
|
344
333
|
"encoded_site_logo.txt",
|
|
@@ -369,7 +358,7 @@ const getEntryPoint = (roleId, state, mobileConfig) => {
|
|
|
369
358
|
// device is ready
|
|
370
359
|
export async function init(mobileConfig) {
|
|
371
360
|
try {
|
|
372
|
-
if (Capacitor.
|
|
361
|
+
if (Capacitor.getPlatform() === "web") {
|
|
373
362
|
defineCustomElements(window);
|
|
374
363
|
await customElements.whenDefined("jeep-sqlite");
|
|
375
364
|
const jeepSqlite = document.createElement("jeep-sqlite");
|
|
@@ -381,6 +370,28 @@ export async function init(mobileConfig) {
|
|
|
381
370
|
await saltcorn.mobileApp.navigation.goBack(1, true);
|
|
382
371
|
});
|
|
383
372
|
|
|
373
|
+
App.addListener("appUrlOpen", async (event) => {
|
|
374
|
+
try {
|
|
375
|
+
const url = event.url;
|
|
376
|
+
if (url.startsWith("mobileapp://auth/callback")) {
|
|
377
|
+
const token = new URL(url).searchParams.get("token");
|
|
378
|
+
const method = new URL(url).searchParams.get("method");
|
|
379
|
+
const methods = saltcorn.data.state.getState().auth_methods;
|
|
380
|
+
if (!methods[method])
|
|
381
|
+
throw new Error(`Authentication method '${method}' not found.`);
|
|
382
|
+
const modName = methods[method].module_name;
|
|
383
|
+
if (!modName)
|
|
384
|
+
throw new Error(`Module name for '${method}' is not defined.`);
|
|
385
|
+
const authModule = saltcorn.mobileApp.plugins[modName];
|
|
386
|
+
if (!authModule)
|
|
387
|
+
throw new Error(`Authentication module '${modName}' not found.`);
|
|
388
|
+
await authModule.finishLogin(token);
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
await showErrorPage(error);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
384
395
|
const lastLocation = takeLastLocation();
|
|
385
396
|
document.addEventListener("resume", onResume, false);
|
|
386
397
|
await addScripts(mobileConfig.version_tag);
|
|
@@ -417,10 +428,10 @@ export async function init(mobileConfig) {
|
|
|
417
428
|
state.mobileConfig.networkState = (
|
|
418
429
|
await Network.getStatus()
|
|
419
430
|
).connectionType;
|
|
420
|
-
if (Capacitor.
|
|
431
|
+
if (Capacitor.getPlatform() === "android") {
|
|
421
432
|
const shareData = await checkSendIntentReceived();
|
|
422
433
|
if (shareData) return await postShare(shareData);
|
|
423
|
-
} else if (Capacitor.
|
|
434
|
+
} else if (Capacitor.getPlatform() === "ios") {
|
|
424
435
|
window.addEventListener("sendIntentReceived", async () => {
|
|
425
436
|
const shareData = await checkSendIntentReceived();
|
|
426
437
|
if (shareData && notEmpty(shareData)) return await postShare(shareData);
|
|
@@ -475,7 +486,7 @@ export async function init(mobileConfig) {
|
|
|
475
486
|
}
|
|
476
487
|
}
|
|
477
488
|
|
|
478
|
-
if (Capacitor.
|
|
489
|
+
if (Capacitor.getPlatform() === "ios") {
|
|
479
490
|
const shareData = await checkSendIntentReceived();
|
|
480
491
|
if (shareData && notEmpty(shareData)) return await postShare(shareData);
|
|
481
492
|
}
|
|
@@ -33,11 +33,21 @@ const prepareAuthForm = () => {
|
|
|
33
33
|
const getAuthLinks = (current, entryPoint) => {
|
|
34
34
|
const links = { methods: [] };
|
|
35
35
|
const state = saltcorn.data.state.getState();
|
|
36
|
+
const mobileConfig = state.mobileConfig;
|
|
36
37
|
if (current !== "login") links.login = "javascript:execLink('/auth/login')";
|
|
37
38
|
if (current !== "signup" && state.getConfig("allow_signup"))
|
|
38
39
|
links.signup = "javascript:execLink('/auth/signup')";
|
|
39
|
-
if (
|
|
40
|
+
if (mobileConfig.showContinueAsPublicUser)
|
|
40
41
|
links.publicUser = `javascript:publicLogin('${entryPoint}')`;
|
|
42
|
+
for (const [name, auth] of Object.entries(state.auth_methods)) {
|
|
43
|
+
links.methods.push({
|
|
44
|
+
icon: auth.icon,
|
|
45
|
+
label: auth.label,
|
|
46
|
+
name,
|
|
47
|
+
url: `javascript:loginWith('${name}')`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
return links;
|
|
42
52
|
};
|
|
43
53
|
|
|
@@ -74,6 +84,8 @@ const renderLoginView = async (entryPoint, versionTag, alerts = []) => {
|
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
const assets_by_role = state.assets_by_role || {};
|
|
88
|
+
const roleHeaders = assets_by_role[100];
|
|
77
89
|
return layout.authWrap({
|
|
78
90
|
title: "login",
|
|
79
91
|
form: form,
|
|
@@ -82,8 +94,10 @@ const renderLoginView = async (entryPoint, versionTag, alerts = []) => {
|
|
|
82
94
|
headers: [
|
|
83
95
|
{ css: `static_assets/${versionTag}/saltcorn.css` },
|
|
84
96
|
{ script: "js/iframe_view_utils.js" },
|
|
97
|
+
...(roleHeaders ? roleHeaders : []),
|
|
85
98
|
],
|
|
86
99
|
csrfToken: false,
|
|
100
|
+
req: new MobileRequest(),
|
|
87
101
|
});
|
|
88
102
|
};
|
|
89
103
|
|
|
@@ -104,6 +118,11 @@ const renderSignupView = (entryPoint, versionTag) => {
|
|
|
104
118
|
});
|
|
105
119
|
};
|
|
106
120
|
|
|
121
|
+
/**
|
|
122
|
+
*
|
|
123
|
+
* @param {*} context
|
|
124
|
+
* @returns
|
|
125
|
+
*/
|
|
107
126
|
export const getLoginView = async (context) => {
|
|
108
127
|
const mobileConfig = saltcorn.data.state.getState().mobileConfig;
|
|
109
128
|
return {
|
|
@@ -116,6 +135,10 @@ export const getLoginView = async (context) => {
|
|
|
116
135
|
};
|
|
117
136
|
};
|
|
118
137
|
|
|
138
|
+
/**
|
|
139
|
+
*
|
|
140
|
+
* @returns
|
|
141
|
+
*/
|
|
119
142
|
export const getSignupView = async () => {
|
|
120
143
|
const config = saltcorn.data.state.getState().mobileConfig;
|
|
121
144
|
return {
|
|
@@ -124,6 +147,10 @@ export const getSignupView = async () => {
|
|
|
124
147
|
};
|
|
125
148
|
};
|
|
126
149
|
|
|
150
|
+
/**
|
|
151
|
+
*
|
|
152
|
+
* @returns
|
|
153
|
+
*/
|
|
127
154
|
export const logoutAction = async () => {
|
|
128
155
|
const config = saltcorn.data.state.getState().mobileConfig;
|
|
129
156
|
const response = await apiCall({ method: "GET", path: "/auth/logout" });
|
|
@@ -46,7 +46,7 @@ export const postShare = async (context) => {
|
|
|
46
46
|
saltcorn.markup.tags.script(`
|
|
47
47
|
setTimeout(() => {
|
|
48
48
|
${
|
|
49
|
-
Capacitor.
|
|
49
|
+
Capacitor.getPlatform() === "android"
|
|
50
50
|
? "parent.saltcorn.mobileApp.common.finishShareIntent();"
|
|
51
51
|
: "parent.saltcorn.mobileApp.navigation.gotoEntryView();"
|
|
52
52
|
}
|
package/src/routing/utils.js
CHANGED
|
@@ -50,7 +50,9 @@ const getMenu = (req) => {
|
|
|
50
50
|
const role = mobileCfg.user.role_id || 100;
|
|
51
51
|
const extraMenu = saltcorn.data.web_mobile_commons.get_extra_menu(
|
|
52
52
|
role,
|
|
53
|
-
req.__
|
|
53
|
+
req.__,
|
|
54
|
+
undefined,
|
|
55
|
+
req
|
|
54
56
|
);
|
|
55
57
|
if (mobileCfg.inErrorState) {
|
|
56
58
|
const entryLink = mobileCfg.entry_point?.startsWith("get")
|
package/www/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
if (window.saltcorn) window.saltcorn.mobileApp = mobileApp;
|
|
17
17
|
else window.saltcorn = { mobileApp };
|
|
18
18
|
|
|
19
|
-
if (Capacitor.
|
|
19
|
+
if (Capacitor.getPlatform() !== "web") {
|
|
20
20
|
document.addEventListener("deviceready", () => {
|
|
21
21
|
saltcorn.mobileApp.init(_sc_mobile_config);
|
|
22
22
|
});
|
|
@@ -124,6 +124,8 @@ async function formSubmit(e, urlSuffix, viewname, noSubmitCb, matchingState) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function sc_form_submit_in_progress() {}
|
|
128
|
+
|
|
127
129
|
async function ajaxSubmitForm(e, force_no_reload, event) {
|
|
128
130
|
const form = $(e).closest("form");
|
|
129
131
|
const action = form.attr("action");
|
|
@@ -285,6 +287,24 @@ async function loginFormSubmit(e, entryView) {
|
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
async function loginWith(strategyName) {
|
|
291
|
+
try {
|
|
292
|
+
console.log("login with", strategyName);
|
|
293
|
+
const methods = parent.saltcorn.data.state.getState().auth_methods;
|
|
294
|
+
if (!methods[strategyName])
|
|
295
|
+
throw new Error(`No such auth strategy: ${strategyName}`);
|
|
296
|
+
const modName = methods[strategyName].module_name;
|
|
297
|
+
if (!modName)
|
|
298
|
+
throw new Error(`Module name for '${strategyName}' is not defined.`);
|
|
299
|
+
const authModule = parent.saltcorn.mobileApp.plugins[modName];
|
|
300
|
+
if (!authModule)
|
|
301
|
+
throw new Error(`Authentication module '${modName}' not found.`);
|
|
302
|
+
await authModule.startLogin(strategyName);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
parent.saltcorn.mobileApp.common.errorAlert(error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
288
308
|
async function local_post_btn(e) {
|
|
289
309
|
try {
|
|
290
310
|
showLoadSpinner();
|
|
@@ -405,8 +425,8 @@ async function pjax_to(href, query, e) {
|
|
|
405
425
|
let $dest = localizer.length
|
|
406
426
|
? localizer
|
|
407
427
|
: inModal
|
|
408
|
-
|
|
409
|
-
|
|
428
|
+
? $("#scmodal .modal-body")
|
|
429
|
+
: $("#page-inner-content");
|
|
410
430
|
if (!$dest.length)
|
|
411
431
|
await parent.saltcorn.mobileApp.navigation.handleRoute(safeHref, query);
|
|
412
432
|
else
|