Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ RUN yarn install --frozen-lockfile
# Copy source files
COPY . .

# Build the web application without environment variables
# Environment variables will be injected at runtime via docker-entrypoint.sh
RUN yarn web:build
# Build the web application with production defaults
# Runtime environment variables will be injected at startup via docker-entrypoint.sh
# APP_ENV=production ensures the build uses production defaults and no .env suffix on IDs
RUN APP_ENV=production yarn web:build

### STAGE 2: Run ###
FROM nginx:1.25-alpine
Expand All @@ -42,6 +43,8 @@ EXPOSE 80
ENV APP_ENV=production \
UNIT_NAME="Resgrid Unit" \
UNIT_SCHEME="ResgridUnit" \
UNIT_BUNDLE_ID="com.resgrid.unit" \
UNIT_PACKAGE="com.resgrid.unit" \
UNIT_VERSION="0.0.1" \
UNIT_BASE_API_URL="https://api.resgrid.com" \
UNIT_API_VERSION="v4" \
Expand Down
2 changes: 2 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'android.permission.POST_NOTIFICATIONS',
'android.permission.FOREGROUND_SERVICE',
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
'android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE',
'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK',
'android.permission.READ_PHONE_STATE',
'android.permission.READ_PHONE_NUMBERS',
'android.permission.MANAGE_OWN_CALLS',
],
},
Expand Down
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = function (api) {
extensions: ['.ios.ts', '.android.ts', '.ts', '.ios.tsx', '.android.tsx', '.tsx', '.jsx', '.js', '.json'],
},
],
'babel-plugin-transform-import-meta',
'react-native-reanimated/plugin',
],
};
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ services:
- APP_ENV=production
- UNIT_NAME=Resgrid Unit
- UNIT_SCHEME=ResgridUnit
- UNIT_BUNDLE_ID=com.resgrid.unit
- UNIT_PACKAGE=com.resgrid.unit
- UNIT_VERSION=0.0.1
- UNIT_BASE_API_URL=https://api.resgrid.com
- UNIT_API_VERSION=v4
Expand Down
4 changes: 4 additions & 0 deletions docker/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ js_escape() {
}

# Create the env-config.js file with environment variables
# Includes ALL fields expected by the client env schema in env.js
cat > "${HTML_DIR}/env-config.js" << EOF
// Runtime environment configuration - generated by docker-entrypoint.sh
// This file is generated at container startup and injects environment variables
window.__ENV__ = {
APP_ENV: "$(js_escape "${APP_ENV:-production}")",
NAME: "$(js_escape "${UNIT_NAME:-Resgrid Unit}")",
SCHEME: "$(js_escape "${UNIT_SCHEME:-ResgridUnit}")",
BUNDLE_ID: "$(js_escape "${UNIT_BUNDLE_ID:-com.resgrid.unit}")",
PACKAGE: "$(js_escape "${UNIT_PACKAGE:-com.resgrid.unit}")",
VERSION: "$(js_escape "${UNIT_VERSION:-0.0.1}")",
ANDROID_VERSION_CODE: 1,
BASE_API_URL: "$(js_escape "${UNIT_BASE_API_URL:-https://api.resgrid.com}")",
API_VERSION: "$(js_escape "${UNIT_API_VERSION:-v4}")",
RESGRID_API_URL: "$(js_escape "${UNIT_RESGRID_API_URL:-/api/v4}")",
Expand Down
54 changes: 50 additions & 4 deletions electron/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
/* eslint-disable no-undef */
const { app, BrowserWindow, ipcMain, Notification, nativeTheme, Menu } = require('electron');
const { app, BrowserWindow, ipcMain, Notification, nativeTheme, Menu, protocol, net } = require('electron');
const path = require('path');
const fs = require('fs');

// Register custom protocol scheme before app is ready
// This allows serving the Expo web export with absolute paths (/_expo/static/...)
// via a custom protocol instead of file://, which breaks absolute path resolution.
protocol.registerSchemesAsPrivileged([
{
scheme: 'app',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
},
},
]);

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) {
Expand Down Expand Up @@ -33,9 +50,14 @@ function createWindow() {
});

// Load the app
const startUrl = isDev ? 'http://localhost:8081' : `file://${path.join(__dirname, '../dist/index.html')}`;

mainWindow.loadURL(startUrl);
if (isDev) {
// In development, load from the Expo dev server
mainWindow.loadURL('http://localhost:8081');
} else {
// In production, load via the custom app:// protocol
// which correctly resolves absolute paths (/_expo/static/...) from the dist directory
mainWindow.loadURL('app://bundle/index.html');
}

// Show window when ready
mainWindow.once('ready-to-show', () => {
Expand Down Expand Up @@ -154,6 +176,30 @@ ipcMain.handle('get-platform', () => {

// Handle app ready
app.whenReady().then(() => {
// Register custom protocol handler for serving the Expo web export
// This resolves absolute paths like /_expo/static/js/... from the dist directory
const distPath = path.join(__dirname, '..', 'dist');

protocol.handle('app', (request) => {
const url = new URL(request.url);
// Decode the pathname and resolve to a file in dist/
let filePath = path.join(distPath, decodeURIComponent(url.pathname));

// If the path points to a directory or file doesn't exist, fall back to index.html
// This supports SPA client-side routing
try {
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
filePath = path.join(distPath, 'index.html');
}
} catch {
// File not found - serve index.html for client-side routing
filePath = path.join(distPath, 'index.html');
}

return net.fetch('file://' + filePath);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

createMenu();
createWindow();

Expand Down
22 changes: 22 additions & 0 deletions global.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Web-only pulse animation for user location marker on map */
@keyframes pulse-ring {
0%, 100% {
transform: scale(1);
opacity: 0.3;
}
50% {
transform: scale(1.2);
opacity: 0.15;
}
}

/* Web-only skeleton loading animation */
@keyframes skeleton-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.75;
}
}
46 changes: 46 additions & 0 deletions metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,52 @@ config.resolver.resolveRequest = (context, moduleName, platform) => {
type: 'sourceFile',
};
}

// Countly SDK needs its own shim with proper default export.
// The CountlyConfig subpath must resolve to a dedicated shim whose
// default export is the CountlyConfig class (not the Countly object).
if (moduleName === 'countly-sdk-react-native-bridge/CountlyConfig') {
return {
filePath: path.resolve(__dirname, 'src/lib/countly-config-shim.web.ts'),
type: 'sourceFile',
};
}
if (moduleName === 'countly-sdk-react-native-bridge' || moduleName.startsWith('countly-sdk-react-native-bridge/')) {
return {
filePath: path.resolve(__dirname, 'src/lib/countly-shim.web.ts'),
type: 'sourceFile',
};
}

// Force zustand and related packages to use CJS build instead of ESM
// The ESM build uses import.meta.env which Metro doesn't support
const zustandModules = {
zustand: path.resolve(__dirname, 'node_modules/zustand/index.js'),
'zustand/shallow': path.resolve(__dirname, 'node_modules/zustand/shallow.js'),
'zustand/middleware': path.resolve(__dirname, 'node_modules/zustand/middleware.js'),
'zustand/traditional': path.resolve(__dirname, 'node_modules/zustand/traditional.js'),
'zustand/vanilla': path.resolve(__dirname, 'node_modules/zustand/vanilla.js'),
'zustand/context': path.resolve(__dirname, 'node_modules/zustand/context.js'),
};

if (zustandModules[moduleName]) {
return {
filePath: zustandModules[moduleName],
type: 'sourceFile',
};
}

// Block build-time/dev packages that use import.meta from being bundled
// These are dev tools that should never be included in a client bundle
const buildTimePackages = ['tinyglobby', 'fdir', 'node-gyp', 'electron-builder', 'electron-rebuild', '@electron/rebuild', 'app-builder-lib', 'dmg-builder'];

if (buildTimePackages.some((pkg) => moduleName === pkg || moduleName.startsWith(`${pkg}/`))) {
// Return an empty module shim
return {
filePath: path.resolve(__dirname, 'src/lib/empty-module.web.js'),
type: 'sourceFile',
};
}
}

// Use the original resolver for everything else
Expand Down
6 changes: 4 additions & 2 deletions nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@ http {
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self';" always;
# Note: CSP connect-src/img-src must allow the configured API URL, Mapbox, Sentry, etc.
# The docker-entrypoint.sh can generate a tighter policy if needed.
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; worker-src 'self' blob:;" always;

# Static assets with cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self';" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https: wss:; worker-src 'self' blob:;" always;
try_files $uri =404;
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
"@typescript-eslint/eslint-plugin": "~5.62.0",
"@typescript-eslint/parser": "~5.62.0",
"babel-jest": "~30.0.0",
"babel-plugin-transform-import-meta": "^2.3.3",
"concurrently": "9.2.1",
"cross-env": "~7.0.3",
"dotenv": "~16.4.5",
Expand Down
Loading
Loading