@saltcorn/mobile-app 1.5.0-beta.8 → 1.5.0-rc.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_sync.js +91 -0
- package/optional_sources/notifications.js +172 -0
- package/package.json +1 -3
- package/src/helpers/auth.js +99 -70
- package/src/helpers/common.js +125 -27
- package/src/helpers/navigation.js +4 -4
- package/src/helpers/offline_mode.js +190 -69
- package/src/index.js +22 -2
- package/src/init.js +17 -4
- package/src/routing/index.js +3 -7
- package/src/routing/routes/auth.js +24 -24
- package/www/js/iframe_view_utils.js +30 -34
- package/build_scripts/modify_android_manifest.js +0 -53
- package/build_scripts/modify_gradle_cfg.js +0 -21
- package/src/helpers/notifications.js +0 -92
- /package/{src/.eslintrc → .eslintrc} +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*global saltcorn*/
|
|
2
|
+
|
|
3
|
+
import { Capacitor } from "@capacitor/core";
|
|
4
|
+
import { apiCall } from "./api";
|
|
5
|
+
import { showToasts } from "./common";
|
|
6
|
+
import { PushNotifications } from "@capacitor/push-notifications";
|
|
7
|
+
import { Device } from "@capacitor/device";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* internal helper to subscribe or unsubscribe to push notifications (server side)
|
|
11
|
+
*/
|
|
12
|
+
async function notifyTokenApi(config, isSubscribe) {
|
|
13
|
+
console.log("notifyTokenApi subscribe:", isSubscribe);
|
|
14
|
+
const { token, deviceId } = config.pushConfiguration;
|
|
15
|
+
try {
|
|
16
|
+
const response = await apiCall({
|
|
17
|
+
method: "POST",
|
|
18
|
+
path: `/notifications/mobile-${isSubscribe ? "subscribe" : "remove-subscription"}`,
|
|
19
|
+
body: { token, deviceId },
|
|
20
|
+
});
|
|
21
|
+
const data = response.data;
|
|
22
|
+
if (data.success === "ok")
|
|
23
|
+
console.log(
|
|
24
|
+
`successfully ${isSubscribe ? "subscribed" : "unsubscribed"} to push notifications`,
|
|
25
|
+
data
|
|
26
|
+
);
|
|
27
|
+
else
|
|
28
|
+
console.error(
|
|
29
|
+
`unable to ${isSubscribe ? "subscribe" : "unsubscribe"} to push notifications`,
|
|
30
|
+
data
|
|
31
|
+
);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(
|
|
34
|
+
`unable to ${isSubscribe ? "subscribe" : "unsubscribe"} to push notifications`,
|
|
35
|
+
error
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* internal helper to subscribe or unsubscribe to push sync (server side)
|
|
42
|
+
* @param {*} config
|
|
43
|
+
* @param {*} isSubscribe
|
|
44
|
+
*/
|
|
45
|
+
async function syncTokenApi(config, isSubscribe) {
|
|
46
|
+
console.log("syncTokenApi subscribe:", isSubscribe);
|
|
47
|
+
const { token, deviceId } = config.pushConfiguration;
|
|
48
|
+
try {
|
|
49
|
+
const response = await apiCall({
|
|
50
|
+
method: "POST",
|
|
51
|
+
path: `/sync/push_${isSubscribe ? "subscribe" : "unsubscribe"}`,
|
|
52
|
+
body: { token, deviceId, synchedTables: config.synchedTables },
|
|
53
|
+
});
|
|
54
|
+
const data = response.data;
|
|
55
|
+
if (data.success === "ok")
|
|
56
|
+
console.log(
|
|
57
|
+
`successfully ${isSubscribe ? "subscribed" : "unsubscribed"} to push sync`,
|
|
58
|
+
data
|
|
59
|
+
);
|
|
60
|
+
else
|
|
61
|
+
console.error(
|
|
62
|
+
`unable to ${isSubscribe ? "subscribe" : "unsubscribe"} to push sync`,
|
|
63
|
+
data
|
|
64
|
+
);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(
|
|
67
|
+
`unable to ${isSubscribe ? "subscribe" : "unsubscribe"} to push sync`,
|
|
68
|
+
error
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function messageHandler(notification) {
|
|
74
|
+
console.log("Push received:", notification);
|
|
75
|
+
const state = saltcorn.data.state.getState();
|
|
76
|
+
const type = notification.data?.type;
|
|
77
|
+
if (type && state.mobile_push_handler?.[type]) {
|
|
78
|
+
try {
|
|
79
|
+
await state.mobile_push_handler[type](notification);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`Error handling '${type}' push notification:`, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let registrationListener = null;
|
|
87
|
+
let registrationErrorListener = null;
|
|
88
|
+
let pushReceivedListener = null;
|
|
89
|
+
|
|
90
|
+
export async function initPushNotifications() {
|
|
91
|
+
if (Capacitor.getPlatform() !== "web" && PushNotifications) {
|
|
92
|
+
await removePushListeners();
|
|
93
|
+
const permStatus = await PushNotifications.requestPermissions();
|
|
94
|
+
if (permStatus.receive === "granted") {
|
|
95
|
+
await PushNotifications.register();
|
|
96
|
+
registrationListener = PushNotifications.addListener(
|
|
97
|
+
"registration",
|
|
98
|
+
async (token) => {
|
|
99
|
+
console.log("Push registration success, token:", token.value);
|
|
100
|
+
const config = saltcorn.data.state.getState().mobileConfig;
|
|
101
|
+
if (config.pushConfiguration) {
|
|
102
|
+
console.log("Push already registered");
|
|
103
|
+
} else {
|
|
104
|
+
const { identifier } = await Device.getId();
|
|
105
|
+
config.pushConfiguration = {
|
|
106
|
+
token: token.value,
|
|
107
|
+
deviceId: identifier,
|
|
108
|
+
};
|
|
109
|
+
await notifyTokenApi(config, true);
|
|
110
|
+
if (config.allowOfflineMode && config.pushSync)
|
|
111
|
+
await syncTokenApi(config, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
registrationErrorListener = PushNotifications.addListener(
|
|
117
|
+
"registrationError",
|
|
118
|
+
(err) => {
|
|
119
|
+
console.error("Push registration error:", err);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
pushReceivedListener = PushNotifications.addListener(
|
|
124
|
+
"pushNotificationReceived",
|
|
125
|
+
messageHandler
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
console.warn("Push notification permission not granted");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function unregisterPushNotifications() {
|
|
134
|
+
if (Capacitor.getPlatform() !== "web" && PushNotifications) {
|
|
135
|
+
try {
|
|
136
|
+
await PushNotifications.unregister();
|
|
137
|
+
const config = saltcorn.data.state.getState().mobileConfig;
|
|
138
|
+
await notifyTokenApi(config, false);
|
|
139
|
+
if (config.allowOfflineMode && config.pushSync)
|
|
140
|
+
await syncTokenApi(config, false);
|
|
141
|
+
await removePushListeners();
|
|
142
|
+
config.pushConfiguration = null;
|
|
143
|
+
console.log("Push notifications unregistered successfully");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("Error unregistering push notifications:", error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function addPusNotifyHandler() {
|
|
151
|
+
const state = saltcorn.data.state.getState();
|
|
152
|
+
state.mobile_push_handler["push_notification"] = (notification) => {
|
|
153
|
+
console.log("Push notification received:", notification);
|
|
154
|
+
showToasts([
|
|
155
|
+
{
|
|
156
|
+
type: "info",
|
|
157
|
+
msg: notification.body,
|
|
158
|
+
title: notification.title,
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function removePushListeners() {
|
|
165
|
+
await registrationListener?.remove();
|
|
166
|
+
await registrationErrorListener?.remove();
|
|
167
|
+
await pushReceivedListener?.remove();
|
|
168
|
+
|
|
169
|
+
registrationListener = null;
|
|
170
|
+
registrationErrorListener = null;
|
|
171
|
+
pushReceivedListener = null;
|
|
172
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/mobile-app",
|
|
3
3
|
"displayName": "Saltcorn mobile app",
|
|
4
|
-
"version": "1.5.0-
|
|
4
|
+
"version": "1.5.0-rc.1",
|
|
5
5
|
"description": "Saltcorn mobile app for Android and iOS",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "webpack",
|
|
9
9
|
"add-platform": "npx cap add",
|
|
10
|
-
"modify-gradle-cfg": "node build_scripts/modify_gradle_cfg.js",
|
|
11
|
-
"modify-android-manifest": "node build_scripts/modify_android_manifest.js",
|
|
12
10
|
"test": "echo NO TESTS"
|
|
13
11
|
},
|
|
14
12
|
"author": "Christian Hugo",
|
package/src/helpers/auth.js
CHANGED
|
@@ -5,12 +5,14 @@ import i18next from "i18next";
|
|
|
5
5
|
import { apiCall } from "./api";
|
|
6
6
|
import { router } from "../routing/index";
|
|
7
7
|
import { getLastOfflineSession, deleteOfflineData, sync } from "./offline_mode";
|
|
8
|
-
import { addRoute, replaceIframe } from "../helpers/navigation";
|
|
9
|
-
import { showAlerts } from "./common";
|
|
8
|
+
import { addRoute, replaceIframe, clearHistory } from "../helpers/navigation";
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
showToasts,
|
|
11
|
+
tryInitBackgroundSync,
|
|
12
|
+
tryInitPush,
|
|
13
|
+
tryStopBackgroundSync,
|
|
14
|
+
tryUnregisterPush,
|
|
15
|
+
} from "./common";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* internal helper for the normal login/signup and public login
|
|
@@ -45,80 +47,98 @@ async function loginRequest({ email, password, isSignup, isPublic }) {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
|
-
* helper
|
|
49
|
-
* @param {string}
|
|
50
|
+
* internal helper to process a JWT token
|
|
51
|
+
* @param {string} tokenStr
|
|
50
52
|
*/
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
config.user = decodedJwt.user;
|
|
53
|
+
const handleToken = async (tokenStr, config) => {
|
|
54
|
+
const token = jwtDecode(tokenStr);
|
|
55
|
+
const user = token.user;
|
|
56
|
+
config.user = user;
|
|
56
57
|
config.isPublicUser = false;
|
|
57
58
|
config.isOfflineMode = false;
|
|
58
|
-
await insertUser(
|
|
59
|
-
await setJwt(
|
|
60
|
-
config.jwt =
|
|
61
|
-
i18next.changeLanguage(
|
|
59
|
+
await insertUser(user);
|
|
60
|
+
await setJwt(tokenStr);
|
|
61
|
+
config.jwt = tokenStr;
|
|
62
|
+
i18next.changeLanguage(user.language);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* internal helper to run the first sync
|
|
67
|
+
*/
|
|
68
|
+
const initialSync = async (config) => {
|
|
62
69
|
const alerts = [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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();
|
|
70
|
+
const { offlineUser, hasOfflineData } = (await getLastOfflineSession()) || {};
|
|
71
|
+
if (!offlineUser || offlineUser === config.user.email) {
|
|
72
|
+
await sync(true, true, alerts);
|
|
82
73
|
} else {
|
|
83
|
-
|
|
74
|
+
if (hasOfflineData)
|
|
75
|
+
alerts.push({
|
|
76
|
+
type: "warning",
|
|
77
|
+
msg: `'${offlineUser}' has not yet uploaded offline data.`,
|
|
78
|
+
});
|
|
79
|
+
else {
|
|
80
|
+
await deleteOfflineData(true);
|
|
81
|
+
await sync(true, true, alerts);
|
|
82
|
+
}
|
|
84
83
|
}
|
|
85
|
-
alerts
|
|
86
|
-
|
|
87
|
-
msg: i18next.t("Welcome, %s!", {
|
|
88
|
-
postProcess: "sprintf",
|
|
89
|
-
sprintf: [config.user.email],
|
|
90
|
-
}),
|
|
91
|
-
});
|
|
84
|
+
return alerts;
|
|
85
|
+
};
|
|
92
86
|
|
|
87
|
+
/**
|
|
88
|
+
* internal helper to get the path to the first page
|
|
89
|
+
*/
|
|
90
|
+
const getEntryPoint = (config) => {
|
|
93
91
|
let entryPoint = null;
|
|
94
92
|
if (config.entryPointType === "byrole") {
|
|
93
|
+
const state = saltcorn.data.state.getState();
|
|
95
94
|
const homepageByRole = state.getConfig("home_page_by_role", {})[
|
|
96
95
|
config.user.role_id
|
|
97
96
|
];
|
|
98
97
|
if (homepageByRole) entryPoint = `get/page/${homepageByRole}`;
|
|
99
98
|
else throw new Error("No homepage defined for this role.");
|
|
100
99
|
} else entryPoint = config.entry_point;
|
|
100
|
+
return entryPoint;
|
|
101
|
+
};
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
isSignup,
|
|
116
|
-
});
|
|
103
|
+
/**
|
|
104
|
+
* For normal login/signup email and password are used
|
|
105
|
+
* When called from auth provider login (see google-auth plugin), token is used
|
|
106
|
+
* @param {*} param0
|
|
107
|
+
*/
|
|
108
|
+
export async function login({ email, password, isSignup, token }) {
|
|
109
|
+
const loginResult = !token
|
|
110
|
+
? await loginRequest({
|
|
111
|
+
email,
|
|
112
|
+
password,
|
|
113
|
+
isSignup,
|
|
114
|
+
})
|
|
115
|
+
: token;
|
|
117
116
|
if (typeof loginResult === "string") {
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const alerts = [];
|
|
118
|
+
const config = saltcorn.data.state.getState().mobileConfig;
|
|
119
|
+
await handleToken(loginResult, config);
|
|
120
|
+
if (config.allowOfflineMode) alerts.push(await initialSync(config));
|
|
121
|
+
await tryInitPush(config);
|
|
122
|
+
await tryInitBackgroundSync(config);
|
|
123
|
+
alerts.push({
|
|
124
|
+
type: "success",
|
|
125
|
+
msg: i18next.t("Welcome, %s!", {
|
|
126
|
+
postProcess: "sprintf",
|
|
127
|
+
sprintf: [config.user.email],
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// open first page
|
|
132
|
+
const entryPoint = getEntryPoint(config);
|
|
133
|
+
addRoute({ route: entryPoint, query: undefined });
|
|
134
|
+
const page = await router.resolve({
|
|
135
|
+
pathname: entryPoint,
|
|
136
|
+
fullWrap: true,
|
|
137
|
+
alerts,
|
|
138
|
+
});
|
|
139
|
+
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
120
140
|
} else if (loginResult?.alerts) {
|
|
121
|
-
|
|
141
|
+
showToasts(loginResult?.alerts);
|
|
122
142
|
} else {
|
|
123
143
|
throw new Error("The login failed.");
|
|
124
144
|
}
|
|
@@ -160,13 +180,13 @@ export async function publicLogin(entryPoint) {
|
|
|
160
180
|
});
|
|
161
181
|
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
162
182
|
} else if (loginResult?.alerts) {
|
|
163
|
-
|
|
183
|
+
showToasts(loginResult?.alerts);
|
|
164
184
|
} else {
|
|
165
185
|
throw new Error("The login failed.");
|
|
166
186
|
}
|
|
167
187
|
} catch (error) {
|
|
168
188
|
console.error(error);
|
|
169
|
-
|
|
189
|
+
showToasts([
|
|
170
190
|
{
|
|
171
191
|
type: "error",
|
|
172
192
|
msg: error.message ? error.message : "An error occured.",
|
|
@@ -179,14 +199,23 @@ export async function publicLogin(entryPoint) {
|
|
|
179
199
|
export async function logout() {
|
|
180
200
|
try {
|
|
181
201
|
const config = saltcorn.data.state.getState().mobileConfig;
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
await tryUnregisterPush();
|
|
203
|
+
await tryStopBackgroundSync();
|
|
204
|
+
const response = await apiCall({ method: "GET", path: "/auth/logout" });
|
|
205
|
+
if (response.data.success) {
|
|
206
|
+
await removeJwt();
|
|
207
|
+
clearHistory();
|
|
208
|
+
config.jwt = undefined;
|
|
209
|
+
const page = await router.resolve({
|
|
210
|
+
pathname: "get/auth/login",
|
|
211
|
+
entryView: config.entry_point,
|
|
212
|
+
versionTag: config.version_tag,
|
|
213
|
+
});
|
|
214
|
+
await replaceIframe(page.content);
|
|
215
|
+
} else throw new Error("Unable to logout.");
|
|
188
216
|
} catch (error) {
|
|
189
|
-
|
|
217
|
+
console.error("unable to logout:", error);
|
|
218
|
+
showToasts([
|
|
190
219
|
{
|
|
191
220
|
type: "error",
|
|
192
221
|
msg: error.message ? error.message : "An error occured.",
|