Skip to content

Commit f1f8423

Browse files
committed
RU-T46 PR#202 fixes
1 parent 88517a7 commit f1f8423

18 files changed

Lines changed: 222 additions & 73 deletions

File tree

.github/workflows/react-native-cicd.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ on:
3131
- ios
3232
- all
3333

34+
# Set minimal permissions by default
35+
permissions:
36+
contents: read
37+
3438
env:
3539
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
3640
EXPO_APPLE_ID: ${{ secrets.EXPO_APPLE_ID }}
@@ -115,6 +119,8 @@ jobs:
115119
build-mobile:
116120
needs: test
117121
if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) || github.event_name == 'workflow_dispatch'
122+
permissions:
123+
contents: write # Required for creating releases
118124
strategy:
119125
matrix:
120126
platform: [android, ios]
@@ -519,17 +525,29 @@ jobs:
519525
build-docker:
520526
needs: build-web
521527
if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) || github.event_name == 'workflow_dispatch'
528+
permissions:
529+
contents: read
530+
packages: write # Required for pushing to GHCR
522531
runs-on: ubuntu-latest
523532
environment: RNBuild
524533
steps:
525534
- name: 🏗 Checkout repository
526535
uses: actions/checkout@v4
527536

537+
- name: � Check Docker Hub credentials availability
538+
id: docker-creds
539+
run: |
540+
if [[ -n "${{ secrets.DOCKER_USERNAME }}" && -n "${{ secrets.DOCKER_PASSWORD }}" ]]; then
541+
echo "available=true" >> $GITHUB_OUTPUT
542+
else
543+
echo "available=false" >> $GITHUB_OUTPUT
544+
fi
545+
528546
- name: 🐳 Set up Docker Buildx
529547
uses: docker/setup-buildx-action@v3
530548

531549
- name: 🐳 Log in to Docker Hub
532-
if: ${{ secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }}
550+
if: steps.docker-creds.outputs.available == 'true'
533551
uses: docker/login-action@v3
534552
with:
535553
username: ${{ secrets.DOCKER_USERNAME }}
@@ -547,7 +565,7 @@ jobs:
547565
uses: docker/metadata-action@v5
548566
with:
549567
images: |
550-
${{ secrets.DOCKER_USERNAME != '' && format('{0}/resgrid-unit', secrets.DOCKER_USERNAME) || '' }}
568+
${{ steps.docker-creds.outputs.available == 'true' && format('{0}/resgrid-unit', secrets.DOCKER_USERNAME) || '' }}
551569
ghcr.io/${{ github.repository }}
552570
tags: |
553571
type=raw,value=7.${{ github.run_number }}
@@ -571,6 +589,8 @@ jobs:
571589
build-electron:
572590
needs: test
573591
if: (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) || github.event_name == 'workflow_dispatch'
592+
permissions:
593+
contents: write # Required for creating releases
574594
strategy:
575595
matrix:
576596
os: [windows-latest, macos-15, ubuntu-latest]

docker/docker-entrypoint.sh

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,33 @@ set -e
44
# Directory where the built web app is served
55
HTML_DIR="/usr/share/nginx/html"
66

7+
# JavaScript escape function to safely escape strings for JSON/JS
8+
# Escapes quotes, backslashes, and newlines to prevent injection
9+
js_escape() {
10+
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/$/\\n/' | tr -d '\n' | sed 's/\\n$//'
11+
}
12+
713
# Create the env-config.js file with environment variables
814
cat > "${HTML_DIR}/env-config.js" << EOF
915
// Runtime environment configuration - generated by docker-entrypoint.sh
1016
// This file is generated at container startup and injects environment variables
1117
window.__ENV__ = {
12-
APP_ENV: "${APP_ENV:-production}",
13-
NAME: "${UNIT_NAME:-Resgrid Unit}",
14-
SCHEME: "${UNIT_SCHEME:-ResgridUnit}",
15-
VERSION: "${UNIT_VERSION:-0.0.1}",
16-
BASE_API_URL: "${UNIT_BASE_API_URL:-https://api.resgrid.com}",
17-
API_VERSION: "${UNIT_API_VERSION:-v4}",
18-
RESGRID_API_URL: "${UNIT_RESGRID_API_URL:-/api/v4}",
19-
CHANNEL_HUB_NAME: "${UNIT_CHANNEL_HUB_NAME:-eventingHub}",
20-
REALTIME_GEO_HUB_NAME: "${UNIT_REALTIME_GEO_HUB_NAME:-geolocationHub}",
21-
LOGGING_KEY: "${UNIT_LOGGING_KEY:-}",
22-
APP_KEY: "${UNIT_APP_KEY:-}",
23-
UNIT_MAPBOX_PUBKEY: "${UNIT_MAPBOX_PUBKEY:-}",
24-
IS_MOBILE_APP: false,
25-
SENTRY_DSN: "${UNIT_SENTRY_DSN:-}",
26-
COUNTLY_APP_KEY: "${UNIT_COUNTLY_APP_KEY:-}",
27-
COUNTLY_SERVER_URL: "${UNIT_COUNTLY_SERVER_URL:-}"
18+
APP_ENV: "$(js_escape "${APP_ENV:-production}")",
19+
NAME: "$(js_escape "${UNIT_NAME:-Resgrid Unit}")",
20+
SCHEME: "$(js_escape "${UNIT_SCHEME:-ResgridUnit}")",
21+
VERSION: "$(js_escape "${UNIT_VERSION:-0.0.1}")",
22+
BASE_API_URL: "$(js_escape "${UNIT_BASE_API_URL:-https://api.resgrid.com}")",
23+
API_VERSION: "$(js_escape "${UNIT_API_VERSION:-v4}")",
24+
RESGRID_API_URL: "$(js_escape "${UNIT_RESGRID_API_URL:-/api/v4}")",
25+
CHANNEL_HUB_NAME: "$(js_escape "${UNIT_CHANNEL_HUB_NAME:-eventingHub}")",
26+
REALTIME_GEO_HUB_NAME: "$(js_escape "${UNIT_REALTIME_GEO_HUB_NAME:-geolocationHub}")",
27+
LOGGING_KEY: "$(js_escape "${UNIT_LOGGING_KEY:-}")",
28+
APP_KEY: "$(js_escape "${UNIT_APP_KEY:-}")",
29+
UNIT_MAPBOX_PUBKEY: "$(js_escape "${UNIT_MAPBOX_PUBKEY:-}")",
30+
IS_MOBILE_APP: "false",
31+
SENTRY_DSN: "$(js_escape "${UNIT_SENTRY_DSN:-}")",
32+
COUNTLY_APP_KEY: "$(js_escape "${UNIT_COUNTLY_APP_KEY:-}")",
33+
COUNTLY_SERVER_URL: "$(js_escape "${UNIT_COUNTLY_SERVER_URL:-}")"
2834
};
2935
EOF
3036

electron/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ function createWindow() {
2525
// MacOS: use hidden title bar with traffic lights
2626
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
2727
// Windows/Linux: show frame
28-
frame: process.platform !== 'darwin' || true,
28+
frame: process.platform !== 'darwin',
2929
// Set the background color to match the app theme
3030
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1a1a1a' : '#ffffff',
3131
icon: path.join(__dirname, '../assets/icon.png'),

electron/preload.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
1010
// Notification methods
1111
showNotification: (options) => ipcRenderer.invoke('show-notification', options),
1212
onNotificationClicked: (callback) => {
13-
ipcRenderer.on('notification-clicked', (event, data) => callback(data));
14-
// Return a cleanup function
13+
const listener = (event, data) => callback(data);
14+
ipcRenderer.on('notification-clicked', listener);
15+
// Return a cleanup function that removes only this specific listener
1516
return () => {
16-
ipcRenderer.removeAllListeners('notification-clicked');
17+
ipcRenderer.removeListener('notification-clicked', listener);
1718
};
1819
},
1920

env.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const client = z.object({
8888
LOGGING_KEY: z.string(),
8989
APP_KEY: z.string(),
9090
UNIT_MAPBOX_PUBKEY: z.string(),
91-
IS_MOBILE_APP: z.boolean(),
91+
IS_MOBILE_APP: z.string(),
9292
SENTRY_DSN: z.string(),
9393
COUNTLY_APP_KEY: z.string(),
9494
COUNTLY_SERVER_URL: z.string(),
@@ -120,7 +120,7 @@ const _clientEnv = {
120120
REALTIME_GEO_HUB_NAME: process.env.UNIT_REALTIME_GEO_HUB_NAME || 'geolocationHub',
121121
LOGGING_KEY: process.env.UNIT_LOGGING_KEY || '',
122122
APP_KEY: process.env.UNIT_APP_KEY || '',
123-
IS_MOBILE_APP: true, // or whatever default you want
123+
IS_MOBILE_APP: 'true',
124124
UNIT_MAPBOX_PUBKEY: process.env.UNIT_MAPBOX_PUBKEY || '',
125125
SENTRY_DSN: process.env.UNIT_SENTRY_DSN || '',
126126
COUNTLY_APP_KEY: process.env.UNIT_COUNTLY_APP_KEY || '',

nginx.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ http {
4444
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
4545
expires 1y;
4646
add_header Cache-Control "public, immutable";
47+
add_header X-Frame-Options "SAMEORIGIN" always;
48+
add_header X-Content-Type-Options "nosniff" always;
49+
add_header X-XSS-Protection "1; mode=block" always;
4750
try_files $uri =404;
4851
}
4952

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@
197197
"dotenv": "~16.4.5",
198198
"electron": "40.0.0",
199199
"electron-builder": "26.4.0",
200+
"electron-squirrel-startup": "^1.0.1",
200201
"eslint": "~8.57.0",
201202
"eslint-config-expo": "~7.1.2",
202203
"eslint-config-prettier": "~9.1.0",

public/service-worker.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
// Cache name for offline support (optional)
99
const CACHE_NAME = 'resgrid-unit-v1';
1010

11+
// Store pending notification data for newly opened windows
12+
const pendingNotifications = new Map();
13+
1114
// Handle push events
1215
self.addEventListener('push', function (event) {
1316
console.log('[Service Worker] Push received:', event);
@@ -80,15 +83,11 @@ self.addEventListener('notificationclick', function (event) {
8083
// Open new window if no existing window found
8184
if (clients.openWindow) {
8285
return clients.openWindow('/').then(function (client) {
83-
// Send message after a short delay to ensure the app is ready
84-
setTimeout(function () {
85-
if (client) {
86-
client.postMessage({
87-
type: 'NOTIFICATION_CLICK',
88-
data: data,
89-
});
90-
}
91-
}, 1000);
86+
// Store notification data for handshake with the new window
87+
if (client) {
88+
pendingNotifications.set(client.id, data);
89+
console.log('[Service Worker] Stored pending notification for client:', client.id);
90+
}
9291
});
9392
}
9493
})
@@ -118,7 +117,27 @@ self.addEventListener('activate', function (event) {
118117
self.addEventListener('message', function (event) {
119118
console.log('[Service Worker] Message received:', event.data);
120119

120+
// Handle skip waiting message
121121
if (event.data && event.data.type === 'SKIP_WAITING') {
122122
self.skipWaiting();
123+
return;
124+
}
125+
126+
// Handle client ready handshake
127+
if (event.data && event.data.type === 'CLIENT_READY') {
128+
const clientId = event.source.id;
129+
console.log('[Service Worker] Client ready handshake received:', clientId);
130+
131+
// Check if there's a pending notification for this client
132+
if (pendingNotifications.has(clientId)) {
133+
const notificationData = pendingNotifications.get(clientId);
134+
pendingNotifications.delete(clientId);
135+
136+
console.log('[Service Worker] Sending pending notification to client:', clientId);
137+
event.source.postMessage({
138+
type: 'NOTIFICATION_CLICK',
139+
data: notificationData,
140+
});
141+
}
123142
}
124143
});

src/app/call/__tests__/[id].test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ const mockUseWindowDimensions = useWindowDimensions as jest.MockedFunction<typeo
8383
jest.mock('expo-constants', () => ({
8484
expoConfig: {
8585
extra: {
86-
IS_MOBILE_APP: true,
86+
IS_MOBILE_APP: "true",
8787
},
8888
},
8989
default: {
9090
expoConfig: {
9191
extra: {
92-
IS_MOBILE_APP: true,
92+
IS_MOBILE_APP: "true",
9393
},
9494
},
9595
},
@@ -98,7 +98,7 @@ jest.mock('expo-constants', () => ({
9898
// Mock @env to prevent expo-constants issues
9999
jest.mock('@env', () => ({
100100
Env: {
101-
IS_MOBILE_APP: true,
101+
IS_MOBILE_APP: "true",
102102
},
103103
}));
104104

src/components/maps/map-view.web.tsx

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'mapbox-gl/dist/mapbox-gl.css';
66

77
import mapboxgl from 'mapbox-gl';
88
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
9+
import ReactDOM from 'react-dom';
910

1011
import { Env } from '@/lib/env';
1112

@@ -238,7 +239,7 @@ interface PointAnnotationProps {
238239
coordinate: [number, number];
239240
title?: string;
240241
children?: React.ReactNode;
241-
anchor?: { x: number; y: number };
242+
anchor?: string | { x: number; y: number };
242243
onSelected?: () => void;
243244
}
244245

@@ -256,32 +257,61 @@ export const PointAnnotation: React.FC<PointAnnotationProps> = ({ id, coordinate
256257
container.style.cursor = 'pointer';
257258
containerRef.current = container;
258259

259-
// If there are children, render them into the container
260+
// Render React children into the container using createPortal
260261
if (children) {
261-
// Simple case - just show a marker
262-
container.innerHTML = '<div style="width: 24px; height: 24px; background: #3b82f6; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>';
262+
ReactDOM.render(<>{children}</>, container);
263263
}
264264

265-
markerRef.current = new mapboxgl.Marker({
265+
// Determine marker options based on anchor prop
266+
const markerOptions: mapboxgl.MarkerOptions = {
266267
element: container,
267-
anchor: 'center',
268-
})
268+
};
269+
270+
// Handle anchor prop - if it's a string, use it as mapbox anchor
271+
if (typeof anchor === 'string') {
272+
markerOptions.anchor = anchor as mapboxgl.Anchor;
273+
}
274+
275+
markerRef.current = new mapboxgl.Marker(markerOptions)
269276
.setLngLat(coordinate)
270277
.addTo(map);
271278

279+
// If anchor is an {x, y} object, convert to pixel offset
280+
if (typeof anchor === 'object' && anchor !== null && 'x' in anchor && 'y' in anchor) {
281+
// Calculate offset based on container size
282+
// Mapbox expects offset in pixels, anchor is typically 0-1 range
283+
// Convert anchor position to offset (center is anchor {0.5, 0.5})
284+
const rect = container.getBoundingClientRect();
285+
const xOffset = (anchor.x - 0.5) * rect.width;
286+
const yOffset = (anchor.y - 0.5) * rect.height;
287+
markerRef.current.setOffset([xOffset, yOffset]);
288+
}
289+
272290
if (title) {
273291
markerRef.current.setPopup(new mapboxgl.Popup().setText(title));
274292
}
275293

276-
if (onSelected) {
277-
container.addEventListener('click', onSelected);
294+
// Attach click listener to container
295+
if (onSelected && containerRef.current) {
296+
containerRef.current.addEventListener('click', onSelected);
278297
}
279298

280299
return () => {
300+
// Clean up click listener
301+
if (onSelected && containerRef.current) {
302+
containerRef.current.removeEventListener('click', onSelected);
303+
}
304+
305+
// Unmount React children from the portal
306+
if (children && containerRef.current) {
307+
ReactDOM.unmountComponentAtNode(containerRef.current);
308+
}
309+
310+
// Remove marker from map
281311
markerRef.current?.remove();
282312
};
283313
// eslint-disable-next-line react-hooks/exhaustive-deps
284-
}, [map, coordinate, id]);
314+
}, [map, coordinate, id, children, anchor, onSelected, title]);
285315

286316
// Update position when coordinate changes
287317
useEffect(() => {
@@ -317,9 +347,19 @@ export const UserLocation: React.FC<UserLocationProps> = ({ visible = true, show
317347
map.addControl(geolocate);
318348

319349
// Auto-trigger to show user location
320-
map.on('load', () => {
350+
if (map.loaded()) {
321351
geolocate.trigger();
322-
});
352+
} else {
353+
const onMapLoad = () => {
354+
geolocate.trigger();
355+
};
356+
map.on('load', onMapLoad);
357+
358+
return () => {
359+
map.off('load', onMapLoad);
360+
map.removeControl(geolocate);
361+
};
362+
}
323363

324364
return () => {
325365
map.removeControl(geolocate);

0 commit comments

Comments
 (0)