Skip to content

Commit 2758657

Browse files
authored
Merge pull request #100 from Resgrid/develop
Develop
2 parents 99a6443 + 4d6e4d0 commit 2758657

94 files changed

Lines changed: 5722 additions & 882 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

__mocks__/expo-auth-session.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Mock for expo-auth-session
2+
export const ResponseType = {
3+
Code: 'code',
4+
Token: 'token',
5+
IdToken: 'id_token',
6+
};
7+
8+
export const makeRedirectUri = jest.fn().mockReturnValue('test://auth/callback');
9+
10+
export const useAutoDiscovery = jest.fn().mockReturnValue({
11+
authorizationEndpoint: 'https://example.com/oauth/authorize',
12+
tokenEndpoint: 'https://example.com/oauth/token',
13+
});
14+
15+
export const useAuthRequest = jest.fn().mockReturnValue([
16+
null, // request
17+
null, // response
18+
jest.fn().mockResolvedValue({ type: 'cancel' }), // promptAsync
19+
]);
20+
21+
export const exchangeCodeAsync = jest.fn().mockResolvedValue({
22+
idToken: 'mock-id-token',
23+
accessToken: 'mock-access-token',
24+
});
25+
26+
export const AuthSessionRedirectUriOptions = {};

__mocks__/expo-crypto.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Mock for expo-crypto
2+
export const digestStringAsync = jest.fn().mockResolvedValue('mock-hash');
3+
4+
export const CryptoDigestAlgorithm = {
5+
SHA256: 'SHA-256',
6+
SHA512: 'SHA-512',
7+
SHA1: 'SHA-1',
8+
MD2: 'MD2',
9+
MD4: 'MD4',
10+
MD5: 'MD5',
11+
};
12+
13+
export const CryptoEncoding = {
14+
HEX: 'hex',
15+
BASE64: 'base64',
16+
};
17+
18+
export const getRandomBytesAsync = jest.fn().mockResolvedValue(new Uint8Array(32));
19+
20+
export const randomUUID = jest.fn().mockReturnValue('mock-uuid-1234');

__mocks__/expo-web-browser.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Mock for expo-web-browser
2+
export const maybeCompleteAuthSession = jest.fn().mockReturnValue({ type: 'success' });
3+
4+
export const openBrowserAsync = jest.fn().mockResolvedValue({ type: 'cancel' });
5+
6+
export const openAuthSessionAsync = jest.fn().mockResolvedValue({ type: 'cancel' });
7+
8+
export const dismissBrowser = jest.fn();

__mocks__/react-native-mmkv.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,36 @@
55

66
import { useState } from 'react';
77

8+
const fallbackStores: Map<string, Record<string, string>> = new Map();
9+
10+
function getFallbackStorage(id: string): Storage {
11+
let existing = fallbackStores.get(id);
12+
if (existing === undefined) {
13+
existing = {};
14+
fallbackStores.set(id, existing);
15+
}
16+
const _fallbackData: Record<string, string> = existing;
17+
const _fallbackStorage: Storage = {
18+
getItem(key: string) { return _fallbackData[key] ?? null; },
19+
setItem(key: string, value: string) { _fallbackData[key] = value; },
20+
removeItem(key: string) { delete _fallbackData[key]; },
21+
key(index: number) { return Object.keys(_fallbackData)[index] ?? null; },
22+
get length() { return Object.keys(_fallbackData).length; },
23+
clear() { Object.keys(_fallbackData).forEach((k) => { delete _fallbackData[k]; }); },
24+
} as unknown as Storage;
25+
return _fallbackStorage;
26+
}
27+
828
class MockMMKV {
929
private storage: Storage;
1030
private prefix: string;
1131

1232
constructor(config?: { id?: string; encryptionKey?: string }) {
13-
this.storage = typeof window !== 'undefined' ? window.localStorage : ({} as Storage);
14-
this.prefix = config?.id || 'mmkv';
33+
const id = config?.id || 'mmkv';
34+
this.storage = typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'
35+
? window.localStorage
36+
: getFallbackStorage(id);
37+
this.prefix = id;
1538
}
1639

1740
private getKey(key: string): string {

app.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
5151
ITSAppUsesNonExemptEncryption: false,
5252
UIViewControllerBasedStatusBarAppearance: false,
5353
NSBluetoothAlwaysUsageDescription: 'Allow Resgrid Dispatch to connect to bluetooth devices for PTT.',
54+
LSApplicationQueriesSchemes: [Env.SCHEME, 'https', 'http'],
5455
},
5556
entitlements: {
5657
...((Env.APP_ENV === 'production' || Env.APP_ENV === 'internal') && {
@@ -72,6 +73,14 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
7273
},
7374
softwareKeyboardLayoutMode: 'pan',
7475
package: Env.PACKAGE,
76+
intentFilters: [
77+
{
78+
action: 'VIEW',
79+
autoVerify: true,
80+
data: [{ scheme: Env.SCHEME }],
81+
category: ['BROWSABLE', 'DEFAULT'],
82+
},
83+
],
7584
...(fs.existsSync(path.join(__dirname, 'google-services.json')) && {
7685
googleServicesFile: 'google-services.json',
7786
}),
@@ -268,6 +277,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
268277
'@livekit/react-native-expo-plugin',
269278
'@config-plugins/react-native-webrtc',
270279
'@config-plugins/react-native-callkeep',
280+
'expo-web-browser',
271281
'./customGradle.plugin.js',
272282
'./customManifest.plugin.js',
273283
['app-icon-badge', appIconBadgeConfig],

assets/js/form-render.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/js/jquery-3.6.0.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

electron/main.js

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { app, BrowserWindow, protocol, net } = require('electron');
1+
const { app, BrowserWindow, protocol, net, Notification, ipcMain, session } = require('electron');
22
const path = require('path');
33
const url = require('url');
44

@@ -31,9 +31,9 @@ function createWindow() {
3131
icon: path.join(__dirname, '../assets/icon.png'),
3232
webPreferences: {
3333
preload: path.join(__dirname, 'preload.js'),
34-
nodeIntegration: isDev,
35-
contextIsolation: !isDev,
36-
webSecurity: !isDev,
34+
nodeIntegration: false,
35+
contextIsolation: true,
36+
webSecurity: true,
3737
},
3838
});
3939

@@ -55,6 +55,43 @@ function createWindow() {
5555
}
5656

5757
app.whenReady().then(() => {
58+
// ── Content Security Policy ───────────────────────────────────────
59+
// Set a proper CSP to silence the Electron security warning about
60+
// "unsafe-eval" / missing CSP. In development we allow the local
61+
// dev-server origin; in production only the custom app:// scheme.
62+
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
63+
let csp;
64+
if (isDev) {
65+
// Dev mode: Metro/webpack needs 'unsafe-eval' for source maps
66+
// and hot-reload, blob: for dynamic chunks, ws: for HMR.
67+
csp =
68+
"default-src 'self' http://localhost:8081;" +
69+
" script-src 'self' http://localhost:8081 'unsafe-inline' 'unsafe-eval' blob:;" +
70+
" style-src 'self' http://localhost:8081 'unsafe-inline';" +
71+
" img-src 'self' http://localhost:8081 data: https: blob:;" +
72+
" font-src 'self' http://localhost:8081 data:;" +
73+
" connect-src 'self' http://localhost:8081 https: wss: ws:;" +
74+
" media-src 'self' http://localhost:8081 data: blob:;" +
75+
" worker-src 'self' blob:;";
76+
} else {
77+
csp =
78+
"default-src 'self' app:;" +
79+
" script-src 'self' app: 'unsafe-inline';" +
80+
" style-src 'self' app: 'unsafe-inline';" +
81+
" img-src 'self' app: data: https:;" +
82+
" font-src 'self' app: data:;" +
83+
" connect-src 'self' app: https: wss:;" +
84+
" media-src 'self' app: data:;" +
85+
" worker-src 'self' blob:;";
86+
}
87+
88+
callback({
89+
responseHeaders: {
90+
...details.responseHeaders,
91+
'Content-Security-Policy': [csp],
92+
},
93+
});
94+
});
5895
// Register the custom app:// protocol handler for production builds.
5996
// This serves all files from the dist/ directory so that absolute asset
6097
// paths in the bundled HTML/JS/CSS resolve correctly.
@@ -77,6 +114,47 @@ app.whenReady().then(() => {
77114

78115
createWindow();
79116

117+
// ── Notification IPC handlers ──────────────────────────────────────
118+
// Allow the renderer to request native Electron Notification objects
119+
// which map to macOS Notification Center, Windows Toast & Linux
120+
// libnotify/notify-send automatically.
121+
122+
ipcMain.handle('notifications:isSupported', () => {
123+
return Notification.isSupported();
124+
});
125+
126+
ipcMain.handle('notifications:show', (_event, payload) => {
127+
if (!Notification.isSupported()) {
128+
console.warn('Native notifications are not supported on this platform');
129+
return false;
130+
}
131+
132+
try {
133+
const notification = new Notification({
134+
title: payload.title || 'Resgrid Dispatch',
135+
body: payload.body || '',
136+
icon: path.join(__dirname, '../assets/icon.png'),
137+
silent: false,
138+
});
139+
140+
notification.on('click', () => {
141+
// Focus / restore the main window when the notification is clicked
142+
const windows = BrowserWindow.getAllWindows();
143+
if (windows.length > 0) {
144+
const win = windows[0];
145+
if (win.isMinimized()) win.restore();
146+
win.focus();
147+
}
148+
});
149+
150+
notification.show();
151+
return true;
152+
} catch (err) {
153+
console.error('Failed to show native notification:', err);
154+
return false;
155+
}
156+
});
157+
80158
app.on('activate', () => {
81159
if (BrowserWindow.getAllWindows().length === 0) {
82160
createWindow();

electron/preload.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,55 @@
11
// preload.js
22
// All of the Node.js APIs are available in the preload process.
33
// It has the same sandbox as a Chrome extension.
4+
const { contextBridge, ipcRenderer } = require('electron');
5+
6+
// ── Expose Electron platform flag ──────────────────────────────────────
7+
// The renderer can use `window.__ELECTRON__` to detect if it is running
8+
// inside Electron (as opposed to a regular browser).
9+
contextBridge.exposeInMainWorld('__ELECTRON__', true);
10+
11+
// ── Expose notification bridge ─────────────────────────────────────────
12+
// Provides a safe bridge for the renderer to trigger native OS
13+
// notifications via the main process (macOS, Windows, Linux).
14+
const notificationCallbacks = [];
15+
16+
contextBridge.exposeInMainWorld('electronNotifications', {
17+
/**
18+
* Show a native OS notification via the Electron main process.
19+
* @param {{ title: string, body: string, eventCode?: string, data?: object }} payload
20+
* @returns {Promise<boolean>} true if the notification was shown
21+
*/
22+
show: (payload) => ipcRenderer.invoke('notifications:show', payload),
23+
24+
/**
25+
* Check whether native notifications are supported on this platform.
26+
* @returns {Promise<boolean>}
27+
*/
28+
isSupported: () => ipcRenderer.invoke('notifications:isSupported'),
29+
30+
/**
31+
* Register a callback that fires when the main process sends a
32+
* notification payload to the renderer (e.g. from a backend push
33+
* channel handled in the main process).
34+
* @param {(payload: object) => void} callback
35+
*/
36+
onNotification: (callback) => {
37+
notificationCallbacks.push(callback);
38+
},
39+
});
40+
41+
// Forward notifications pushed from main → renderer
42+
ipcRenderer.on('notification:push', (_event, payload) => {
43+
for (const cb of notificationCallbacks) {
44+
try {
45+
cb(payload);
46+
} catch (err) {
47+
console.error('Error in notification callback:', err);
48+
}
49+
}
50+
});
51+
52+
// ── Version information (original preload logic) ───────────────────────
453
window.addEventListener('DOMContentLoaded', () => {
554
const replaceText = (selector, text) => {
655
const element = document.getElementById(selector)

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"install-maestro": "curl -Ls 'https://get.maestro.mobile.dev' | bash",
4646
"e2e-test": "maestro test .maestro/ -e APP_ID=com.obytes.development",
4747
"web:build": "cross-env APP_ENV=production expo export --platform web",
48-
"electron:dev": "concurrently \"cross-env BROWSER=none yarn web\" \"wait-on http://localhost:8081 && electron .\"",
48+
"electron:dev": "concurrently \"cross-env NODE_OPTIONS=--max-old-space-size=16384 BROWSER=none yarn web\" \"wait-on http://localhost:8081 && electron electron/main.js\"",
4949
"electron:pack": "yarn web:build && electron-builder -c.extraMetadata.main=electron/main.js",
5050
"docker:build": "docker build -t resgrid-dispatch-web .",
5151
"docker:run": "docker run -p 3000:80 --env-file .env.docker resgrid-dispatch-web",
@@ -151,9 +151,11 @@
151151
"expo-application": "~6.1.5",
152152
"expo-asset": "~11.1.7",
153153
"expo-audio": "~0.4.9",
154+
"expo-auth-session": "~6.2.1",
154155
"expo-av": "~15.1.7",
155156
"expo-build-properties": "~0.14.8",
156157
"expo-constants": "~17.1.7",
158+
"expo-crypto": "~14.1.5",
157159
"expo-dev-client": "~5.2.4",
158160
"expo-device": "~7.1.4",
159161
"expo-document-picker": "~13.1.6",
@@ -175,6 +177,7 @@
175177
"expo-status-bar": "~2.2.3",
176178
"expo-system-ui": "~5.0.11",
177179
"expo-task-manager": "~13.1.6",
180+
"expo-web-browser": "~14.2.0",
178181
"geojson": "~0.5.0",
179182
"he": "^1.2.0",
180183
"i18next": "~23.14.0",
@@ -298,4 +301,4 @@
298301
"resolutions": {
299302
"form-data": "4.0.4"
300303
}
301-
}
304+
}

0 commit comments

Comments
 (0)