@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saltcorn/mobile-app",
3
3
  "displayName": "Saltcorn mobile app",
4
- "version": "1.5.0-beta.0",
4
+ "version": "1.5.0-beta.10",
5
5
  "description": "Saltcorn mobile app for Android and iOS",
6
6
  "main": "index.js",
7
7
  "scripts": {
@@ -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
- const decodedJwt = jwtDecode(loginResult);
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 {
@@ -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 pluginName = key.split("/")[1];
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.platform !== "web") {
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.platform !== "web") {
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 (state) => {
341
- if (Capacitor.platform === "web") return "";
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.platform === "web") {
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.platform === "android") {
431
+ if (Capacitor.getPlatform() === "android") {
421
432
  const shareData = await checkSendIntentReceived();
422
433
  if (shareData) return await postShare(shareData);
423
- } else if (Capacitor.platform === "ios") {
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.platform === "ios") {
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 (state.getConfig("public_user_link"))
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.platform === "android"
49
+ Capacitor.getPlatform() === "android"
50
50
  ? "parent.saltcorn.mobileApp.common.finishShareIntent();"
51
51
  : "parent.saltcorn.mobileApp.navigation.gotoEntryView();"
52
52
  }
@@ -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.platform !== "web") {
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
- ? $("#scmodal .modal-body")
409
- : $("#page-inner-content");
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