@saltcorn/mobile-app 1.5.0-beta.12 → 1.5.0-beta.14
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 +94 -65
- package/src/helpers/common.js +76 -0
- package/src/helpers/offline_mode.js +79 -52
- package/src/index.js +22 -2
- package/src/init.js +15 -2
- package/src/routing/index.js +3 -7
- package/src/routing/routes/auth.js +24 -24
- 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 { showAlerts } 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
|
+
showAlerts([
|
|
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-beta.
|
|
4
|
+
"version": "1.5.0-beta.14",
|
|
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
|
+
showAlerts,
|
|
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,78 +47,96 @@ 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
|
-
|
|
70
|
+
const { offlineUser, hasOfflineData } = (await getLastOfflineSession()) || {};
|
|
71
|
+
if (!offlineUser || offlineUser === config.user.email) {
|
|
72
|
+
await sync();
|
|
73
|
+
} else {
|
|
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);
|
|
67
81
|
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
82
|
}
|
|
79
83
|
}
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
});
|
|
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
|
showAlerts(loginResult?.alerts);
|
|
122
142
|
} else {
|
|
@@ -179,13 +199,22 @@ 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) {
|
|
217
|
+
console.error("unable to logout:", error);
|
|
189
218
|
showAlerts([
|
|
190
219
|
{
|
|
191
220
|
type: "error",
|
package/src/helpers/common.js
CHANGED
|
@@ -4,6 +4,7 @@ import { apiCall } from "./api";
|
|
|
4
4
|
import { Camera, CameraResultType } from "@capacitor/camera";
|
|
5
5
|
import { ScreenOrientation } from "@capacitor/screen-orientation";
|
|
6
6
|
import { SendIntent } from "send-intent";
|
|
7
|
+
import { addPushSyncHandler } from "./offline_mode";
|
|
7
8
|
|
|
8
9
|
const orientationChangeListeners = new Set();
|
|
9
10
|
|
|
@@ -189,3 +190,78 @@ export async function checkSendIntentReceived() {
|
|
|
189
190
|
return null;
|
|
190
191
|
}
|
|
191
192
|
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* init the push system, if available
|
|
196
|
+
*/
|
|
197
|
+
export async function tryInitPush(config) {
|
|
198
|
+
try {
|
|
199
|
+
const { initPushNotifications, addPusNotifyHandler } = await import(
|
|
200
|
+
"../helpers/notifications.js"
|
|
201
|
+
);
|
|
202
|
+
try {
|
|
203
|
+
await initPushNotifications();
|
|
204
|
+
if (saltcorn.data.utils.isPushEnabled(config.user)) addPusNotifyHandler();
|
|
205
|
+
if (config.pushSync) addPushSyncHandler();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error("Error initializing push notifications:", error);
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.log("Push notifications module not available:", error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* init background sync, if available
|
|
216
|
+
*/
|
|
217
|
+
export async function tryInitBackgroundSync(config) {
|
|
218
|
+
try {
|
|
219
|
+
const { startPeriodicBackgroundSync } = await import(
|
|
220
|
+
"../helpers/background_sync.js"
|
|
221
|
+
);
|
|
222
|
+
try {
|
|
223
|
+
if (config.syncInterval && config.syncInterval > 0)
|
|
224
|
+
await startPeriodicBackgroundSync(config.syncInterval);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error("Error initializing background sync:", error);
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.log("Background sync module not available:", error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* end the push system, if available
|
|
235
|
+
*/
|
|
236
|
+
export async function tryUnregisterPush() {
|
|
237
|
+
try {
|
|
238
|
+
const { unregisterPushNotifications } = await import(
|
|
239
|
+
"../helpers/notifications.js"
|
|
240
|
+
);
|
|
241
|
+
try {
|
|
242
|
+
await unregisterPushNotifications();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error("Error unregistering push notifications:", error);
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.log("Push notifications module not available:", error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* stop background sync, if available
|
|
253
|
+
*/
|
|
254
|
+
export async function tryStopBackgroundSync() {
|
|
255
|
+
try {
|
|
256
|
+
const { stopPeriodicBackgroundSync } = await import(
|
|
257
|
+
"../helpers/background_sync.js"
|
|
258
|
+
);
|
|
259
|
+
try {
|
|
260
|
+
await stopPeriodicBackgroundSync();
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error("Error stopping periodic background sync:", error);
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error("Push notifications module not available:", error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -445,57 +445,72 @@ const setSpinnerText = () => {
|
|
|
445
445
|
}
|
|
446
446
|
};
|
|
447
447
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
448
|
+
let syncInProgress = false;
|
|
449
|
+
|
|
450
|
+
export async function isSyncInProgress() {
|
|
451
|
+
return syncInProgress;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export async function sync(background = false) {
|
|
455
|
+
if (syncInProgress)
|
|
456
|
+
throw new Error("A synchronization is already in progress.");
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
if (!background) setSpinnerText();
|
|
460
|
+
const state = saltcorn.data.state.getState();
|
|
461
|
+
const { user } = state.mobileConfig;
|
|
462
|
+
const { offlineUser, hasOfflineData, uploadStarted, uploadStartTime } =
|
|
463
|
+
(await getLastOfflineSession()) || {};
|
|
464
|
+
if (offlineUser && hasOfflineData && offlineUser !== user.email) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`The sync is not available, '${offlineUser}' has not yet uploaded offline data.`
|
|
467
|
+
);
|
|
468
|
+
} else {
|
|
469
|
+
let syncDir = null;
|
|
470
|
+
let cleanSync = await checkCleanSync(
|
|
471
|
+
uploadStarted,
|
|
472
|
+
uploadStartTime,
|
|
473
|
+
user.email
|
|
474
|
+
);
|
|
475
|
+
const syncTimestamp = await getSyncTimestamp();
|
|
476
|
+
await setUploadStarted(true, syncTimestamp);
|
|
477
|
+
let lock = null;
|
|
478
|
+
if (!background) {
|
|
479
|
+
try {
|
|
480
|
+
if (window.navigator?.wakeLock?.request)
|
|
481
|
+
lock = await window.navigator.wakeLock.request();
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.log("wakeLock not available");
|
|
484
|
+
console.log(error);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let transactionOpen = false;
|
|
488
|
+
try {
|
|
489
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = OFF;");
|
|
490
|
+
await saltcorn.data.db.query("BEGIN");
|
|
491
|
+
transactionOpen = true;
|
|
492
|
+
if (cleanSync) await clearLocalData(true);
|
|
493
|
+
const { synchedTables, syncInfos } = await prepare();
|
|
494
|
+
await syncRemoteDeletes(syncInfos, syncTimestamp);
|
|
495
|
+
syncDir = await syncOfflineData(synchedTables, syncTimestamp);
|
|
496
|
+
await syncRemoteData(syncInfos, syncTimestamp);
|
|
497
|
+
if (!background) await endOfflineMode(true);
|
|
498
|
+
await setUploadStarted(false);
|
|
499
|
+
await saltcorn.data.db.query("COMMIT");
|
|
500
|
+
transactionOpen = false;
|
|
501
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (transactionOpen) await saltcorn.data.db.query("ROLLBACK");
|
|
504
|
+
await saltcorn.data.db.query("PRAGMA foreign_keys = ON;");
|
|
505
|
+
console.log(error);
|
|
506
|
+
throw error;
|
|
507
|
+
} finally {
|
|
508
|
+
if (syncDir) await cleanSyncDir(syncDir);
|
|
509
|
+
if (lock) await lock.release();
|
|
510
|
+
}
|
|
498
511
|
}
|
|
512
|
+
} finally {
|
|
513
|
+
syncInProgress = false;
|
|
499
514
|
}
|
|
500
515
|
}
|
|
501
516
|
|
|
@@ -602,7 +617,7 @@ export async function hasOfflineRows() {
|
|
|
602
617
|
const table = saltcorn.data.models.Table.findOne({ name: tblName });
|
|
603
618
|
const pkName = table.pk_name;
|
|
604
619
|
const { rows } = await saltcorn.data.db.query(
|
|
605
|
-
`select count(info_tbl.ref)
|
|
620
|
+
`select count(info_tbl.ref) as total
|
|
606
621
|
from "${saltcorn.data.db.sqlsanitize(
|
|
607
622
|
tblName
|
|
608
623
|
)}_sync_info" as info_tbl
|
|
@@ -610,7 +625,7 @@ export async function hasOfflineRows() {
|
|
|
610
625
|
on info_tbl.ref = data_tbl."${saltcorn.data.db.sqlsanitize(pkName)}"
|
|
611
626
|
where info_tbl.modified_local = true`
|
|
612
627
|
);
|
|
613
|
-
if (rows?.length > 0 && parseInt(rows[0].
|
|
628
|
+
if (rows?.length > 0 && parseInt(rows[0].total) > 0) return true;
|
|
614
629
|
}
|
|
615
630
|
return false;
|
|
616
631
|
}
|
|
@@ -643,3 +658,15 @@ export async function deleteOfflineData(noFeedback) {
|
|
|
643
658
|
if (!noFeedback) removeLoadSpinner();
|
|
644
659
|
}
|
|
645
660
|
}
|
|
661
|
+
|
|
662
|
+
export function addPushSyncHandler() {
|
|
663
|
+
const state = saltcorn.data.state.getState();
|
|
664
|
+
state.mobile_push_handler["push_sync"] = async (notification) => {
|
|
665
|
+
console.log("Push sync received:", notification);
|
|
666
|
+
try {
|
|
667
|
+
await sync(true);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
console.log("Error during push sync:", error);
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,13 +2,32 @@ import { init } 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";
|
|
5
|
-
import * as notifications from "./helpers/notifications";
|
|
6
5
|
import * as fileSystem from "./helpers/file_system";
|
|
7
6
|
import * as navigation from "./helpers/navigation";
|
|
8
7
|
import * as offlineMode from "./helpers/offline_mode";
|
|
9
8
|
import * as dbSchema from "./helpers/db_schema";
|
|
10
9
|
import { router } from "./routing/index";
|
|
11
10
|
|
|
11
|
+
// included when push notifications or push sync is enabled
|
|
12
|
+
let notifications = undefined;
|
|
13
|
+
try {
|
|
14
|
+
notifications = require("./helpers/notifications");
|
|
15
|
+
console.log("Notifications module available");
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.log("Notifications module not available");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// included when periodic background sync is enabled
|
|
21
|
+
let backgroundSync = undefined;
|
|
22
|
+
try {
|
|
23
|
+
backgroundSync = require("./helpers/background_sync");
|
|
24
|
+
console.log("Background sync module available");
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.log("Background sync module not available");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// include code placed in a mobile-app directory inside plugins
|
|
12
31
|
const plugins = {};
|
|
13
32
|
const context = require.context("./plugins-code", true, /index\.js$/);
|
|
14
33
|
context.keys().forEach((key) => {
|
|
@@ -22,10 +41,11 @@ export const mobileApp = {
|
|
|
22
41
|
api,
|
|
23
42
|
auth,
|
|
24
43
|
common,
|
|
25
|
-
notifications,
|
|
26
44
|
fileSystem,
|
|
27
45
|
navigation: { ...navigation, router },
|
|
28
46
|
offlineMode,
|
|
29
47
|
dbSchema,
|
|
48
|
+
...(notifications ? { notifications } : {}),
|
|
49
|
+
...(backgroundSync ? { backgroundSync } : {}),
|
|
30
50
|
plugins,
|
|
31
51
|
};
|
package/src/init.js
CHANGED
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
gotoEntryView,
|
|
23
23
|
addRoute,
|
|
24
24
|
} from "./helpers/navigation.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
checkSendIntentReceived,
|
|
27
|
+
tryInitBackgroundSync,
|
|
28
|
+
tryInitPush,
|
|
29
|
+
} from "./helpers/common.js";
|
|
26
30
|
import { readJSONCordova, readTextCordova } from "./helpers/file_system.js";
|
|
27
31
|
|
|
28
32
|
import i18next from "i18next";
|
|
@@ -443,6 +447,7 @@ export async function init(mobileConfig) {
|
|
|
443
447
|
const jwt = state.mobileConfig.jwt;
|
|
444
448
|
const alerts = [];
|
|
445
449
|
if ((networkDisabled && jwt) || (await checkJWT(jwt))) {
|
|
450
|
+
// already logged in, continue
|
|
446
451
|
const mobileConfig = state.mobileConfig;
|
|
447
452
|
const decodedJwt = jwtDecode(mobileConfig.jwt);
|
|
448
453
|
mobileConfig.user = decodedJwt.user;
|
|
@@ -486,6 +491,9 @@ export async function init(mobileConfig) {
|
|
|
486
491
|
}
|
|
487
492
|
}
|
|
488
493
|
|
|
494
|
+
await tryInitPush(mobileConfig);
|
|
495
|
+
await tryInitBackgroundSync(mobileConfig);
|
|
496
|
+
|
|
489
497
|
if (Capacitor.getPlatform() === "ios") {
|
|
490
498
|
const shareData = await checkSendIntentReceived();
|
|
491
499
|
if (shareData && notEmpty(shareData)) return await postShare(shareData);
|
|
@@ -518,6 +526,7 @@ export async function init(mobileConfig) {
|
|
|
518
526
|
}
|
|
519
527
|
if (page.content) await replaceIframe(page.content, page.isFile);
|
|
520
528
|
} else if (isPublicJwt(jwt)) {
|
|
529
|
+
// already logged in as public
|
|
521
530
|
const config = state.mobileConfig;
|
|
522
531
|
config.user = { role_id: 100, email: "public", language: "en" };
|
|
523
532
|
config.isPublicUser = true;
|
|
@@ -539,13 +548,17 @@ export async function init(mobileConfig) {
|
|
|
539
548
|
)) &&
|
|
540
549
|
state.mobileConfig.autoPublicLogin
|
|
541
550
|
) {
|
|
551
|
+
// try autoPublicLogin
|
|
542
552
|
if (networkDisabled)
|
|
543
553
|
throw new Error(
|
|
544
554
|
"No internet connection or previous login is available. " +
|
|
545
555
|
"Please go online and reload, the public login is not yet supported."
|
|
546
556
|
);
|
|
547
557
|
await publicLogin(getEntryPoint(100, state, state.mobileConfig));
|
|
548
|
-
|
|
558
|
+
} else {
|
|
559
|
+
// open login page
|
|
560
|
+
await showLogin(alerts);
|
|
561
|
+
}
|
|
549
562
|
} catch (error) {
|
|
550
563
|
if (typeof saltcorn === "undefined" || typeof router === "undefined") {
|
|
551
564
|
const msg = `An error occured: ${
|
package/src/routing/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
updateTableRow,
|
|
7
7
|
insertTableRow,
|
|
8
8
|
} from "./routes/api";
|
|
9
|
-
import { getLoginView,
|
|
9
|
+
import { getLoginView, getSignupView } from "./routes/auth";
|
|
10
10
|
import { deleteRows } from "./routes/delete";
|
|
11
11
|
import { postToggleField } from "./routes/edit";
|
|
12
12
|
import { getErrorView } from "./routes/error";
|
|
@@ -54,16 +54,12 @@ const routes = [
|
|
|
54
54
|
path: "get/auth/login",
|
|
55
55
|
action: getLoginView,
|
|
56
56
|
},
|
|
57
|
-
{
|
|
58
|
-
path: "get/auth/logout",
|
|
59
|
-
action: logoutAction,
|
|
60
|
-
},
|
|
61
57
|
{
|
|
62
58
|
path: "get/auth/signup",
|
|
63
59
|
action: getSignupView,
|
|
64
60
|
},
|
|
61
|
+
|
|
65
62
|
// delete
|
|
66
|
-
|
|
67
63
|
{
|
|
68
64
|
path: "post/delete/:tableName/:id", // legacy
|
|
69
65
|
action: deleteRows,
|
|
@@ -72,7 +68,7 @@ const routes = [
|
|
|
72
68
|
path: "delete/api/:tableName/:id",
|
|
73
69
|
action: deleteRows,
|
|
74
70
|
},
|
|
75
|
-
|
|
71
|
+
|
|
76
72
|
// edit
|
|
77
73
|
{
|
|
78
74
|
path: "post/edit/toggle/:name/:id/:field_name",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/*global saltcorn */
|
|
2
2
|
import { MobileRequest } from "../mocks/request";
|
|
3
3
|
import { MobileResponse } from "../mocks/response";
|
|
4
|
-
import { apiCall } from "../../helpers/api";
|
|
5
|
-
import { removeJwt } from "../../helpers/auth";
|
|
6
4
|
import { sbAdmin2Layout, getHeaders } from "../utils";
|
|
7
|
-
import { clearHistory } from "../../helpers/navigation";
|
|
8
5
|
|
|
6
|
+
/**
|
|
7
|
+
* internal helper to prepare the login or signup form
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
9
10
|
const prepareAuthForm = () => {
|
|
10
11
|
return new saltcorn.data.models.Form({
|
|
11
12
|
class: "login",
|
|
@@ -29,7 +30,13 @@ const prepareAuthForm = () => {
|
|
|
29
30
|
});
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* internal helper to get auth links
|
|
35
|
+
* TODO delete this and integrate getAuthLinks() from '/server/auth/routes.js'
|
|
36
|
+
* @param {*} current
|
|
37
|
+
* @param {*} entryPoint
|
|
38
|
+
* @returns
|
|
39
|
+
*/
|
|
33
40
|
const getAuthLinks = (current, entryPoint) => {
|
|
34
41
|
const links = { methods: [] };
|
|
35
42
|
const state = saltcorn.data.state.getState();
|
|
@@ -51,6 +58,13 @@ const getAuthLinks = (current, entryPoint) => {
|
|
|
51
58
|
return links;
|
|
52
59
|
};
|
|
53
60
|
|
|
61
|
+
/**
|
|
62
|
+
* internal helper to render login view
|
|
63
|
+
* @param {*} entryPoint
|
|
64
|
+
* @param {*} versionTag
|
|
65
|
+
* @param {*} alerts
|
|
66
|
+
* @returns
|
|
67
|
+
*/
|
|
54
68
|
const renderLoginView = async (entryPoint, versionTag, alerts = []) => {
|
|
55
69
|
const state = saltcorn.data.state.getState();
|
|
56
70
|
const form = prepareAuthForm(entryPoint);
|
|
@@ -101,6 +115,12 @@ const renderLoginView = async (entryPoint, versionTag, alerts = []) => {
|
|
|
101
115
|
});
|
|
102
116
|
};
|
|
103
117
|
|
|
118
|
+
/**
|
|
119
|
+
* internal helper to render signup view
|
|
120
|
+
* @param {*} entryPoint
|
|
121
|
+
* @param {*} versionTag
|
|
122
|
+
* @returns
|
|
123
|
+
*/
|
|
104
124
|
const renderSignupView = (entryPoint, versionTag) => {
|
|
105
125
|
const form = prepareAuthForm(entryPoint);
|
|
106
126
|
form.onSubmit = `javascript:signupFormSubmit(this, '${entryPoint}')`;
|
|
@@ -146,23 +166,3 @@ export const getSignupView = async () => {
|
|
|
146
166
|
replaceIframe: true,
|
|
147
167
|
};
|
|
148
168
|
};
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
*
|
|
152
|
-
* @returns
|
|
153
|
-
*/
|
|
154
|
-
export const logoutAction = async () => {
|
|
155
|
-
const config = saltcorn.data.state.getState().mobileConfig;
|
|
156
|
-
const response = await apiCall({ method: "GET", path: "/auth/logout" });
|
|
157
|
-
if (response.data.success) {
|
|
158
|
-
await removeJwt();
|
|
159
|
-
clearHistory();
|
|
160
|
-
config.jwt = undefined;
|
|
161
|
-
return {
|
|
162
|
-
content: await renderLoginView(config.entry_point, config.version_tag),
|
|
163
|
-
};
|
|
164
|
-
} else {
|
|
165
|
-
console.log("unable to logout");
|
|
166
|
-
return {};
|
|
167
|
-
}
|
|
168
|
-
};
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
const { parseStringPromise, Builder } = require("xml2js");
|
|
2
|
-
const { join } = require("path");
|
|
3
|
-
const { readFileSync, writeFileSync } = require("fs");
|
|
4
|
-
|
|
5
|
-
const readMobileConfig = () => {
|
|
6
|
-
console.log("Reading mobile config");
|
|
7
|
-
const content = readFileSync(
|
|
8
|
-
"/saltcorn-mobile-app/saltcorn-mobile-cfg.json",
|
|
9
|
-
"utf8"
|
|
10
|
-
);
|
|
11
|
-
console.log(content);
|
|
12
|
-
return JSON.parse(content);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
(async () => {
|
|
16
|
-
try {
|
|
17
|
-
const { permissions, features } = readMobileConfig();
|
|
18
|
-
const androidManifest = join(
|
|
19
|
-
"android",
|
|
20
|
-
"app",
|
|
21
|
-
"src",
|
|
22
|
-
"main",
|
|
23
|
-
"AndroidManifest.xml"
|
|
24
|
-
);
|
|
25
|
-
const content = readFileSync(androidManifest);
|
|
26
|
-
const parsed = await parseStringPromise(content);
|
|
27
|
-
|
|
28
|
-
parsed.manifest["uses-permission"] = permissions.map((p) => ({
|
|
29
|
-
$: { "android:name": p },
|
|
30
|
-
}));
|
|
31
|
-
parsed.manifest["uses-feature"] = features.map((f) => ({
|
|
32
|
-
$: { "android:name": f },
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
parsed.manifest.application[0].$ = {
|
|
36
|
-
...parsed.manifest.application[0].$,
|
|
37
|
-
"android:allowBackup": "false",
|
|
38
|
-
"android:fullBackupContent": "false",
|
|
39
|
-
"android:dataExtractionRules": "@xml/data_extraction_rules",
|
|
40
|
-
"android:networkSecurityConfig": "@xml/network_security_config",
|
|
41
|
-
"android:usesCleartextTraffic": "true",
|
|
42
|
-
};
|
|
43
|
-
const xmlBuilder = new Builder();
|
|
44
|
-
const newCfg = xmlBuilder.buildObject(parsed);
|
|
45
|
-
writeFileSync(androidManifest, newCfg);
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.log(
|
|
48
|
-
`Unable to modify the AndroidManifest.xml: ${
|
|
49
|
-
error.message ? error.message : "Unknown error"
|
|
50
|
-
}`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
})();
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const { join } = require("path");
|
|
2
|
-
const { readFileSync, writeFileSync } = require("fs");
|
|
3
|
-
|
|
4
|
-
console.log("Writing gradle config");
|
|
5
|
-
console.log("args", process.argv);
|
|
6
|
-
const args = process.argv.slice(2);
|
|
7
|
-
const appVersion = args[0].split("=")[1];
|
|
8
|
-
|
|
9
|
-
const gradleFile = join(__dirname, "..", "android", "app", "build.gradle");
|
|
10
|
-
const gradleContent = readFileSync(gradleFile, "utf8");
|
|
11
|
-
|
|
12
|
-
// generate versionCode from appVersion
|
|
13
|
-
const parts = appVersion.split(".");
|
|
14
|
-
const versionCode =
|
|
15
|
-
parseInt(parts[0]) * 1000000 + parseInt(parts[1]) * 1000 + parseInt(parts[2]);
|
|
16
|
-
let newGradleContent = gradleContent
|
|
17
|
-
.replace(/versionName "1.0"/, `versionName "${appVersion}"`)
|
|
18
|
-
.replace(/versionCode 1/, `versionCode ${versionCode}`);
|
|
19
|
-
|
|
20
|
-
console.log("newGradleContent", newGradleContent);
|
|
21
|
-
writeFileSync(gradleFile, newGradleContent, "utf8");
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { Capacitor } from "@capacitor/core";
|
|
2
|
-
import { apiCall } from "./api";
|
|
3
|
-
import { showAlerts } from "./common";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @capacitor/push-notifications isn't always included in the build
|
|
7
|
-
*/
|
|
8
|
-
async function loadNotificationsPlugin() {
|
|
9
|
-
try {
|
|
10
|
-
const { PushNotifications } = await import("@capacitor/push-notifications");
|
|
11
|
-
return PushNotifications;
|
|
12
|
-
} catch (error) {
|
|
13
|
-
console.warn("Error loading PushNotifications plugin:", error);
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @capacitor/device isn't always included in the build
|
|
20
|
-
*/
|
|
21
|
-
async function loadDevicePlugin() {
|
|
22
|
-
try {
|
|
23
|
-
const { Device } = await import("@capacitor/device");
|
|
24
|
-
return Device;
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.warn("Error loading Device plugin:", error);
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function uploadFcmToken(token, deviceId) {
|
|
32
|
-
try {
|
|
33
|
-
const response = await apiCall({
|
|
34
|
-
method: "POST",
|
|
35
|
-
path: "/notifications/fcm-token",
|
|
36
|
-
body: { token, deviceId },
|
|
37
|
-
});
|
|
38
|
-
const data = response.data;
|
|
39
|
-
if (data.success.success === "ok")
|
|
40
|
-
console.log("Token uploaded successfully:", data);
|
|
41
|
-
else console.error("Unable to upload token:", data);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error("Error uploading token:", error);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function initPushNotifications() {
|
|
48
|
-
const PushNotifications = await loadNotificationsPlugin();
|
|
49
|
-
if (Capacitor.getPlatform() !== "web" && PushNotifications) {
|
|
50
|
-
const { Device } = await loadDevicePlugin();
|
|
51
|
-
const permStatus = await PushNotifications.requestPermissions();
|
|
52
|
-
if (permStatus.receive === "granted") {
|
|
53
|
-
await PushNotifications.register();
|
|
54
|
-
PushNotifications.addListener("registration", async (token) => {
|
|
55
|
-
const { identifier } = await Device.getId();
|
|
56
|
-
await uploadFcmToken(token.value, identifier);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
PushNotifications.addListener("registrationError", (err) => {
|
|
60
|
-
console.error("Push registration error:", err);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
PushNotifications.addListener(
|
|
64
|
-
"pushNotificationReceived",
|
|
65
|
-
(notification) => {
|
|
66
|
-
console.log("Push received in foreground:", notification);
|
|
67
|
-
showAlerts([
|
|
68
|
-
{
|
|
69
|
-
type: "info",
|
|
70
|
-
msg: notification.body,
|
|
71
|
-
title: notification.title,
|
|
72
|
-
},
|
|
73
|
-
]);
|
|
74
|
-
}
|
|
75
|
-
);
|
|
76
|
-
} else {
|
|
77
|
-
console.warn("Push notification permission not granted");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function unregisterPushNotifications() {
|
|
83
|
-
const PushNotifications = await loadNotificationsPlugin();
|
|
84
|
-
if (Capacitor.getPlatform() !== "web" && PushNotifications) {
|
|
85
|
-
try {
|
|
86
|
-
await PushNotifications.unregister();
|
|
87
|
-
console.log("Push notifications unregistered successfully");
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.error("Error unregistering push notifications:", error);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
File without changes
|