Merge branch 'local_server' into staging_environment

This commit is contained in:
Carter 2026-05-21 13:11:40 -06:00
commit d864a9c1de
28 changed files with 934 additions and 506 deletions

7
.env
View file

@ -7,6 +7,7 @@ VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com
VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_AUTH0_AUDIENCE=api.brandxtech.ca
VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1
VITE_AUTH0_STAGING_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_AUTH0_STAGING_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP
VITE_AUTH0_STREAMLINE_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX
VITE_AUTH0_BXT_CLIENT_ID=sLnqOu40uWfQT1lYSDYj2wYmlLEHRB74 VITE_AUTH0_BXT_CLIENT_ID=sLnqOu40uWfQT1lYSDYj2wYmlLEHRB74
VITE_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd
@ -16,6 +17,9 @@ VITE_AUTH0_MIVENT_CLIENT_ID=VNALE7RW6l3dY5uYcxgwElZV0lcT25Fg
VITE_AUTH0_OMNIAIR_CLIENT_ID=IblmarD8wFafiD6doxTmOHQ6Bx3L9wWl VITE_AUTH0_OMNIAIR_CLIENT_ID=IblmarD8wFafiD6doxTmOHQ6Bx3L9wWl
VITE_AUTH0_INTELLIFARMS_CLIENT_ID=RYtuAyOcB4DSaaqJMLDMf3pV8SFY9PdY VITE_AUTH0_INTELLIFARMS_CLIENT_ID=RYtuAyOcB4DSaaqJMLDMf3pV8SFY9PdY
#Crisp
VITE_CRISP_WEBSITE_ID=80170383-b426-43c0-8f66-8e20a05bcdce
#Branding (Default theme) #Branding (Default theme)
VITE_APP_WEBSITE_TITLE="Adaptive Dashboard" VITE_APP_WEBSITE_TITLE="Adaptive Dashboard"
VITE_APP_PRIMARY_COLOUR=blue VITE_APP_PRIMARY_COLOUR=blue
@ -24,3 +28,6 @@ VITE_APP_SIGNATURE_COLOUR="#323232"
# Live device/component WebSocket streams (must match string "true" in code) # Live device/component WebSocket streams (must match string "true" in code)
VITE_ENABLE_WEBSOCKETS=true VITE_ENABLE_WEBSOCKETS=true
# Default whitelabel; overwrite in .env.local
#VITE_WHITELABEL=adaptive-ag

67
deploy.sh Executable file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
# Load .env.local if it exists
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env.local"
if [[ -f "$ENV_FILE" ]]; then
# Export only the vars we care about, ignoring comments and blank lines
set -o allexport
# shellcheck source=/dev/null
source <(grep -E '^(DEPLOY_USER|DEPLOY_HOST|DEPLOY_SSH_KEY)=' "$ENV_FILE")
set +o allexport
fi
# Prompt for any missing values
if [[ -z "${DEPLOY_USER:-}" ]]; then
read -rp "SSH username: " DEPLOY_USER
fi
if [[ -z "${DEPLOY_HOST:-}" ]]; then
read -rp "Server IP/hostname: " DEPLOY_HOST
fi
if [[ -z "${DEPLOY_SSH_KEY:-}" ]]; then
read -rp "SSH key path [~/.ssh/id_ed25519]: " DEPLOY_SSH_KEY
DEPLOY_SSH_KEY="${DEPLOY_SSH_KEY:-~/.ssh/id_ed25519}"
fi
SSH_OPTS="-i $DEPLOY_SSH_KEY"
echo "→ Deploying to $DEPLOY_USER@$DEPLOY_HOST using key $DEPLOY_SSH_KEY"
# 1. Build frontend for LAN / self-contained hosts (indexLocal.html via vite --mode localnet)
# Pass VITE_WHITELABEL if provided as an argument; otherwise Vite reads it from .env
(
cd "$SCRIPT_DIR"
if [[ -n "${1:-}" ]]; then
echo "→ Whitelabel override: $1"
VITE_WHITELABEL="$1" npm run build:local
else
npm run build:local
fi
)
# Fail if the bundle still embeds Crisp (wrong vite mode or stale build/)
INDEX_HTML="$SCRIPT_DIR/build/index.html"
if [[ ! -f "$INDEX_HTML" ]]; then
echo "error: missing $INDEX_HTML after build:local" >&2
exit 1
fi
# 2. Build container image
docker build -t webui:local -f Dockerfile .
# 3. Transfer image
docker save webui:local | ssh $SSH_OPTS "$DEPLOY_USER@$DEPLOY_HOST" docker load
# 4. SCP compose file
scp $SSH_OPTS docker-compose.local.yml "$DEPLOY_USER@$DEPLOY_HOST:~/"
# 5. Deploy — then force service recreate. Swarm often keeps the old task when the tag stays
# webui:local after docker load, so without --force you still see the previous HTML/JS.
STACK_NAME="${LOCAL_DOCKER_STACK_NAME:-webui}"
SERVICE_NAME="${STACK_NAME}_webui"
ssh $SSH_OPTS "$DEPLOY_USER@$DEPLOY_HOST" \
"docker stack deploy -c docker-compose.local.yml $STACK_NAME && docker service update --force $SERVICE_NAME"

18
docker-compose.local.yml Normal file
View file

@ -0,0 +1,18 @@
version: "3.6"
services:
webui:
image: webui:local
ports:
- "8080:80"
stop_signal: SIGQUIT
deploy:
replicas: 1
update_config:
order: start-first
healthcheck:
test: ["CMD", "curl", "-f", "localhost:80/health"]
interval: 1m
timeout: 10s
retries: 3
start_period: 30s

16
indexLocal.html Normal file
View file

@ -0,0 +1,16 @@
<!-- LAN / self-contained deploy shell: use with `vite --mode localnet` (e.g. build:local, start-local).
Minimal HTML, no third-party bootstraps that call the public internet (Crisp, etc.). -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" id="favicon-link" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title id="title-id">Adaptive Dashboard</title>
<link rel="manifest" id="manifest-link" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script>
</body>
</html>

View file

@ -5,10 +5,10 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 VITE_APP_WS_URL=ws://api.brandxtech.ca/v1/live vite", "start": "VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 VITE_APP_WS_URL=ws://api.brandxtech.ca/v1/live vite",
"start-local": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=http://localhost:50052/v1 VITE_APP_WS_URL=ws://localhost:50052/v1/live VITE_APP_BILLING_URL=http://localhost:50053/v1 VITE_APP_RECLUSE_URL=http://localhost:50054/v1 VITE_APP_GITLAB_URL=http://localhost:50055/v1 vite", "start-local": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=http://localhost:50052/v1 VITE_APP_WS_URL=ws://localhost:50052/v1/live VITE_APP_BILLING_URL=http://localhost:50053/v1 VITE_APP_RECLUSE_URL=http://localhost:50054/v1 VITE_APP_GITLAB_URL=http://localhost:50055/v1 vite --mode localnet",
"start-dev": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 vite", "start-dev": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 vite",
"start-streamline": "VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca vite", "start-streamline": "VITE_WHITELABEL=streamline VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca vite",
"start-staging": "VITE_LOCAL_STAGING=true VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=ws://stagingapi.brandxtech.ca/v1/live VITE_APP_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_AUTH0_AUDIENCE=stagingapi.brandxtech.ca vite", "start-staging": "VITE_WHITELABEL=staging VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=ws://stagingapi.brandxtech.ca/v1/live vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
@ -16,6 +16,7 @@
"build:development": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_DEVELOPMENT} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", "build:development": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_DEVELOPMENT} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build",
"build:production": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", "build:production": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build",
"build:streamline": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build", "build:streamline": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP=${OPEN_WEATHERMAP} vite build",
"build:local": "VITE_AUTH0_CLIENT_ID=local VITE_AUTH0_AUDIENCE=local VITE_AUTH0_CLIENT_DOMAIN=local VITE_APP_API_URL=http://172.16.1.20:50052/v1 VITE_APP_WS_URL=ws://172.16.1.20:50052/v1/live NODE_OPTIONS=--max_old_space_size=4096 vite build --mode localnet",
"build:offline": "npx env-cmd offline,whitelabel npm run build", "build:offline": "npx env-cmd offline,whitelabel npm run build",
"test": "vitest" "test": "vitest"
}, },

View file

@ -1,15 +1,20 @@
import { Auth0Provider } from '@auth0/auth0-react' import { Auth0Provider } from '@auth0/auth0-react'
import { or } from '../utils/types' import { or } from '../utils/types'
import { isAuth0Configured, isAuth0SpaOriginAllowed, shouldMountAuth0Provider } from '../utils/auth0Config'
import AuthWrapper from '../providers/auth' import AuthWrapper from '../providers/auth'
import HTTPProvider from 'providers/http' import HTTPProvider from 'providers/http'
import { useState } from 'react' import { useState } from 'react'
import LoadingScreen from './LoadingScreen' import LoadingScreen from './LoadingScreen'
import LocalAuthPlaceholder from './LocalAuthPlaceholder'
import UserWrapper from './UserWrapper' import UserWrapper from './UserWrapper'
import { getWhitelabel } from 'services/whiteLabel' import { getWhitelabel } from 'services/whiteLabel'
import { AppThemeProvider } from 'theme/AppThemeProvider' import { AppThemeProvider } from 'theme/AppThemeProvider'
import { LocalAuthProvider, Auth0AuthBridge } from '../providers/authContext'
function App() { function App() {
const [token, setToken] = useState<string | undefined>(undefined) const [token, setToken] = useState<string | undefined>(() => {
return localStorage.getItem('local_auth_token') || undefined
})
const whiteLabel = getWhitelabel() const whiteLabel = getWhitelabel()
const manifestPath = "/" + whiteLabel.name.replace(/\s/g, "") + "/manifest.json" const manifestPath = "/" + whiteLabel.name.replace(/\s/g, "") + "/manifest.json"
@ -54,6 +59,29 @@ function App() {
"/libracart" "/libracart"
] ]
if (!shouldMountAuth0Provider()) {
const placeholderReason =
isAuth0Configured() && !isAuth0SpaOriginAllowed() ? 'insecure_origin' : 'unconfigured'
if (!token) {
return (
<AppThemeProvider>
<LocalAuthPlaceholder reason={placeholderReason} setToken={setToken} />
</AppThemeProvider>
)
}
return (
<AppThemeProvider>
<LocalAuthProvider token={token}>
<HTTPProvider token={token}>
<UserWrapper token={token} />
</HTTPProvider>
</LocalAuthProvider>
</AppThemeProvider>
)
}
return ( return (
<AppThemeProvider> <AppThemeProvider>
<Auth0Provider <Auth0Provider
@ -70,9 +98,11 @@ function App() {
{/* <CssBaseline /> */} {/* <CssBaseline /> */}
<AuthWrapper setToken={setToken}> <AuthWrapper setToken={setToken}>
{ token ? { token ?
<HTTPProvider token={token}> <Auth0AuthBridge>
<UserWrapper token={token} /> <HTTPProvider token={token}>
</HTTPProvider> <UserWrapper token={token} />
</HTTPProvider>
</Auth0AuthBridge>
: :
<LoadingScreen <LoadingScreen
message='Loading user profile' message='Loading user profile'

View file

@ -0,0 +1,141 @@
import { Box, Button, CssBaseline, Paper, Stack, TextField, Typography } from '@mui/material'
import { getName } from 'services/whiteLabel'
import { useState } from 'react'
import axios from 'axios'
export type LocalAuthPlaceholderReason = 'unconfigured' | 'insecure_origin'
interface Props {
reason?: LocalAuthPlaceholderReason
setToken?: (token: string) => void
}
export default function LocalAuthPlaceholder(props: Props) {
const { reason = 'unconfigured', setToken } = props
const productName = getName()
const [mode, setMode] = useState<'login' | 'signup'>('login')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [name, setName] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const apiUrl = import.meta.env.VITE_APP_API_URL
const handleSignup = async () => {
setError('')
setLoading(true)
try {
const resp = await axios.post(`${apiUrl}/local-auth/signup`, { email, password, name })
const token = resp.data.token
if (token && setToken) {
localStorage.setItem('local_auth_token', token)
setToken(token)
}
} catch (err: any) {
setError(err.response?.data?.error || 'Signup failed')
} finally {
setLoading(false)
}
}
const handleLogin = async () => {
setError('')
setLoading(true)
try {
const resp = await axios.post(`${apiUrl}/local-auth/login`, { email, password })
const token = resp.data.token
if (token && setToken) {
localStorage.setItem('local_auth_token', token)
setToken(token)
}
} catch (err: any) {
setError(err.response?.data?.error || 'Login failed')
} finally {
setLoading(false)
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (mode === 'signup') {
handleSignup()
} else {
handleLogin()
}
}
const explanation =
reason === 'insecure_origin'
? 'This URL is not a secure context for cloud sign-in. Use local account sign-in instead.'
: 'Local account sign-in for this deployment.'
return (
<>
<CssBaseline />
<Box
sx={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'background.default',
p: 2,
}}
>
<Paper elevation={2} sx={{ maxWidth: 420, width: '100%', p: 4 }}>
<form onSubmit={handleSubmit}>
<Stack spacing={2} alignItems="stretch">
<Typography variant="h5" component="h1">
{productName}
</Typography>
<Typography variant="body2" color="text.secondary">
{explanation}
</Typography>
{mode === 'signup' && (
<TextField
label="Name"
value={name}
onChange={e => setName(e.target.value)}
fullWidth
/>
)}
<TextField
label="Email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
fullWidth
/>
<TextField
label="Password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
fullWidth
/>
{error && (
<Typography variant="body2" color="error">
{error}
</Typography>
)}
<Button variant="contained" size="large" type="submit" disabled={loading}>
{mode === 'signup' ? 'Sign up' : 'Log in'}
</Button>
<Button
variant="text"
size="small"
onClick={() => { setMode(mode === 'login' ? 'signup' : 'login'); setError('') }}
>
{mode === 'login' ? 'Need an account? Sign up' : 'Already have an account? Log in'}
</Button>
</Stack>
</form>
</Paper>
</Box>
</>
)
}

View file

@ -1,7 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import './App.css' import './App.css'
import { useUserAPI } from '../providers/pond/userAPI' import { useUserAPI } from '../providers/pond/userAPI'
import { useAuth0 } from '@auth0/auth0-react'
import { or } from '../utils/types' import { or } from '../utils/types'
import LoadingScreen from './LoadingScreen' import LoadingScreen from './LoadingScreen'
import NavigationContainer from '../navigation/NavigationContainer' import NavigationContainer from '../navigation/NavigationContainer'
@ -12,9 +11,8 @@ import { makeStyles } from '@mui/styles'
import { CssBaseline, Theme } from '@mui/material' import { CssBaseline, Theme } from '@mui/material'
import { AppThemeProvider } from 'theme/AppThemeProvider' import { AppThemeProvider } from 'theme/AppThemeProvider'
import HTTPProvider from 'providers/http' import HTTPProvider from 'providers/http'
import { Crisp } from "crisp-sdk-web";
import { useMobile, useSnackbar } from 'hooks' import { useMobile, useSnackbar } from 'hooks'
import { initCrisp } from '../chat/CrispChat' import { initCrisp, isCrispEnabled } from '../chat/CrispChat'
// import FirmwareLoader from './FirmwareLoader' // import FirmwareLoader from './FirmwareLoader'
const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => { const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => {
@ -64,16 +62,19 @@ export default function UserWrapper(props: Props) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const userAPI = useUserAPI(); const userAPI = useUserAPI();
const classes = useStyles(); const classes = useStyles();
const useAuth = useAuth0();
const hasFetched = useRef(false); const hasFetched = useRef(false);
const [global, setGlobal] = useState<undefined | GlobalState>(undefined) const [global, setGlobal] = useState<undefined | GlobalState>(undefined)
const snackbar = useSnackbar() const snackbar = useSnackbar()
const isMobile = useMobile() const isMobile = useMobile()
const user_id = or(useAuth.user?.sub, "") const user_id = (() => {
try {
const crispInitialized = useRef(false); const payload = JSON.parse(atob(token.split('.')[1]))
Crisp.configure(import.meta.env.VITE_CRISP_WEBSITE_ID); return or(payload.sub, '')
} catch {
return ''
}
})()
const loadUser = useCallback(() => { const loadUser = useCallback(() => {
if (!userAPI.getUserWithTeam) return; if (!userAPI.getUserWithTeam) return;
@ -114,15 +115,14 @@ export default function UserWrapper(props: Props) {
}, [setGlobal]) }, [setGlobal])
useEffect(() => { useEffect(() => {
if (global?.user) { if (!global?.user || !isCrispEnabled()) return;
initCrisp({ initCrisp({
websiteId: import.meta.env.VITE_CRISP_WEBSITE_ID, websiteId: import.meta.env.VITE_CRISP_WEBSITE_ID,
email: global.user.settings.email, email: global.user.settings.email,
nickname: global.user.settings.name || global.user.settings.email, nickname: global.user.settings.name || global.user.settings.email,
phone: global.user.settings.phoneNumber, phone: global.user.settings.phoneNumber,
tokenId: global.user.id(), tokenId: global.user.id(),
}); });
}
}, [global]); }, [global]);
// useEffect(() => { // useEffect(() => {
@ -150,6 +150,7 @@ export default function UserWrapper(props: Props) {
// }, [isMobile]); // }, [isMobile]);
useEffect(() => { useEffect(() => {
if (!isCrispEnabled()) return;
let style = document.getElementById("crisp-offset-override"); let style = document.getElementById("crisp-offset-override");
if (!style) { if (!style) {
style = document.createElement("style"); style = document.createElement("style");

View file

@ -1,5 +1,6 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { StyledEngineProvider } from '@mui/material/styles'
import App from './App' import App from './App'
// I don't consider providing a disabled button to a Tooltip an error. // I don't consider providing a disabled button to a Tooltip an error.
@ -20,6 +21,8 @@ if ('serviceWorker' in navigator) {
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
// <StrictMode> // <StrictMode>
<App /> <StyledEngineProvider injectFirst>
<App />
</StyledEngineProvider>
// </StrictMode>, // </StrictMode>,
) )

View file

@ -28,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) => {
position: "fixed", position: "fixed",
bottom: theme.spacing(8), //for mobile navigator bottom: theme.spacing(8), //for mobile navigator
right: theme.spacing(2), right: theme.spacing(2),
zIndex: theme.zIndex.speedDial,
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
bottom: theme.spacing(1.75) bottom: theme.spacing(1.75)
} }

View file

@ -7,7 +7,7 @@ import Chat from "./Chat";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTeamAPI } from "providers"; import { useTeamAPI } from "providers";
import { closeCrispChat, openCrispChat } from './CrispChat'; import { closeCrispChat, isCrispEnabled, openCrispChat } from './CrispChat';
import RobotIcon from "products/CommonIcons/robotIcon"; import RobotIcon from "products/CommonIcons/robotIcon";
const useStyles = makeStyles<Theme>((theme: Theme) => ({ const useStyles = makeStyles<Theme>((theme: Theme) => ({
@ -68,7 +68,7 @@ export function ChatDrawer(props: Props) {
} }
useEffect(() => { useEffect(() => {
if (open) { if (open && isCrispEnabled()) {
closeCrispChat() closeCrispChat()
} }
}, [open]) }, [open])
@ -108,13 +108,15 @@ export function ChatDrawer(props: Props) {
<Avatar src={team.settings.avatar} /> <Avatar src={team.settings.avatar} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip title={"Chat Assistant"} placement="right"> {isCrispEnabled() && (
<IconButton <Tooltip title={"Chat Assistant"} placement="right">
onClick={openCrisp} <IconButton
> onClick={openCrisp}
<RobotIcon /> >
</IconButton> <RobotIcon />
</Tooltip> </IconButton>
</Tooltip>
)}
<Box width={"100%"} borderBottom={"1px solid grey"} /> <Box width={"100%"} borderBottom={"1px solid grey"} />
{teams.map((t, i)=> { {teams.map((t, i)=> {
if (t.settings?.key === team.key()) return null; if (t.settings?.key === team.key()) return null;

View file

@ -5,6 +5,11 @@ const ANIMATION_DURATION_MS = 300;
let initialized = false; let initialized = false;
export function isCrispEnabled(): boolean {
const id = import.meta.env.VITE_CRISP_WEBSITE_ID;
return typeof id === "string" && id.trim() !== "";
}
/** /**
* Initialize Crisp and immediately hide the default chat button. * Initialize Crisp and immediately hide the default chat button.
* Call this once on app load (e.g., in UserWrapper after user data is ready). * Call this once on app load (e.g., in UserWrapper after user data is ready).
@ -17,6 +22,7 @@ export function initCrisp(opts: {
tokenId?: string; tokenId?: string;
}) { }) {
if (initialized) return; if (initialized) return;
if (!opts.websiteId?.trim()) return;
Crisp.configure(opts.websiteId); Crisp.configure(opts.websiteId);
Crisp.session.reset(); Crisp.session.reset();
@ -71,6 +77,7 @@ function injectCrispStyles() {
* Safe to call from any onClick handler. * Safe to call from any onClick handler.
*/ */
export function openCrispChat() { export function openCrispChat() {
if (!initialized) return;
const chatbox = document.getElementById("crisp-chatbox"); const chatbox = document.getElementById("crisp-chatbox");
if (chatbox) { if (chatbox) {
chatbox.classList.add("crisp-visible"); chatbox.classList.add("crisp-visible");
@ -86,6 +93,7 @@ export function openCrispChat() {
* Close the chat window and hide the widget. * Close the chat window and hide the widget.
*/ */
export function closeCrispChat() { export function closeCrispChat() {
if (!initialized) return;
Crisp.chat.close(); Crisp.chat.close();
hideCrispChatButton(); hideCrispChatButton();
} }

View file

@ -13,14 +13,14 @@ import BinsIcon from "products/Bindapt/BinsIcon";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import { IsAdaptiveAgriculture, isBXT, IsMiVent, IsAdCon, IsOmniAir, IsMiPCA, IsIntellifarms } from "services/whiteLabel"; import { getFeatures, getWhitelabel } from "services/whiteLabel";
import FieldsIcon from "products/AgIcons/FieldsIcon"; import FieldsIcon from "products/AgIcons/FieldsIcon";
import NexusSTIcon from "products/Construction/NexusSTIcon"; import NexusSTIcon from "products/Construction/NexusSTIcon";
import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon"; import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon";
import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon";
import PlaneIcon from "products/AviationIcons/PlaneIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon";
import JobsiteIcon from "products/Construction/JobSiteIcon"; import JobsiteIcon from "products/Construction/JobSiteIcon";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuthContext } from "providers/authContext";
interface Props { interface Props {
sideIsOpen: boolean; sideIsOpen: boolean;
@ -33,23 +33,19 @@ export default function BottomNavigator(props: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const prevLocation = usePrevious(location); const prevLocation = usePrevious(location);
const { isAuthenticated } = useAuth0(); const { isAuthenticated } = useAuthContext();
const [{ user }] = useGlobalState(); const [{ user }] = useGlobalState();
const [route, setRoute] = useState(sideIsOpen ? "side" : ""); const [route, setRoute] = useState(sideIsOpen ? "side" : "");
const isAg = IsAdaptiveAgriculture(); const wl = getWhitelabel();
const isIntel = IsIntellifarms(); const f = getFeatures();
const isMiVent = IsMiVent();
const isAdCon = IsAdCon();
const isOmni = IsOmniAir();
const isMiPCA = IsMiPCA();
const reRoute = useCallback( const reRoute = useCallback(
(path: string) => { (path: string) => {
if (path === "") { if (path === "") {
if (isAg || isIntel) { if (f.bins) {
return "bins"; return "bins";
} }
if (isMiVent) { if (f.ventilation) {
return "ventilation"; return "ventilation";
} }
return "devices"; return "devices";
@ -75,7 +71,7 @@ export default function BottomNavigator(props: Props) {
} }
return path; return path;
}, },
[isAg, isMiVent, isIntel] [f]
); );
const autoDetectRoute = useCallback(() => { const autoDetectRoute = useCallback(() => {
@ -106,27 +102,27 @@ export default function BottomNavigator(props: Props) {
const authenticatedNavigation = () => { const authenticatedNavigation = () => {
return ( return (
<BottomNavigation value={route} onChange={(_, newValue) => handleRouteChange(newValue)}> <BottomNavigation value={route} onChange={(_, newValue) => handleRouteChange(newValue)}>
{isAg || isIntel && ( {f.visualFarm && (
<BottomNavigationAction label="Farm" icon={<FieldsIcon type={getType()} />} value="visualFarm" /> <BottomNavigationAction label="Farm" icon={<FieldsIcon type={getType()} />} value="visualFarm" />
)} )}
{isAg || isIntel && ( {f.bins && (
<BottomNavigationAction label="Bins" icon={<BinsIcon type={getType()} />} value="bins" /> <BottomNavigationAction label="Bins" icon={<BinsIcon type={getType()} />} value="bins" />
)} )}
{isAdCon && ( {f.constructionMap && (
<BottomNavigationAction <BottomNavigationAction
label="Site Map" label="Site Map"
icon={<FieldsIcon type={getType()} />} icon={<FieldsIcon type={getType()} />}
value="constructionMap" value="constructionMap"
/> />
)} )}
{(isOmni || isMiPCA) && ( {f.aviationMap && (
<BottomNavigationAction <BottomNavigationAction
label="Map" label="Map"
icon={<AirportMapIcon type={getType()} />} icon={<AirportMapIcon type={getType()} />}
value="aviationMap" value="aviationMap"
/> />
)} )}
{(isOmni || isMiPCA) && ( {f.terminals && (
<BottomNavigationAction <BottomNavigationAction
label="Terminals" label="Terminals"
icon={<PlaneIcon type={getType()} />} icon={<PlaneIcon type={getType()} />}
@ -137,11 +133,11 @@ export default function BottomNavigator(props: Props) {
<BottomNavigationAction <BottomNavigationAction
label="Devices" label="Devices"
icon={ icon={
(isAg || isIntel) ? ( f.bins ? (
<BindaptIcon type={getType()} /> <BindaptIcon type={getType()} />
) : isAdCon ? ( ) : f.constructionMap ? (
<NexusSTIcon type={getType()} /> <NexusSTIcon type={getType()} />
) : (isOmni || isMiPCA) ? ( ) : f.aviationMap ? (
<OmniAirDeviceIcon type={getType()} /> <OmniAirDeviceIcon type={getType()} />
) : ( ) : (
<DevicesIcon /> <DevicesIcon />
@ -149,22 +145,15 @@ export default function BottomNavigator(props: Props) {
} }
value="devices" value="devices"
/> />
{isAdCon && ( {f.jobsites && (
<BottomNavigationAction <BottomNavigationAction
label="Sites" label="Sites"
icon={<JobsiteIcon type={getType()} />} icon={<JobsiteIcon type={getType()} />}
value="jobsites" value="jobsites"
/> />
)} )}
{/* {isMiVent && (
<BottomNavigationAction
label="Ventilation"
icon={<VentilationIcon type="light" />}
value="ventilation"
/>
)} */}
{isBXT() && user.hasFeature("security") && ( {f.security && user.hasFeature("security") && (
<BottomNavigationAction label="Security" icon={<Security />} value="security" /> <BottomNavigationAction label="Security" icon={<Security />} value="security" />
)} )}
<BottomNavigationAction label="More" icon={<MoreHoriz />} value="more" /> <BottomNavigationAction label="More" icon={<MoreHoriz />} value="more" />

View file

@ -1,7 +1,7 @@
import { lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import LoadingScreen from "../app/LoadingScreen"; import LoadingScreen from "../app/LoadingScreen";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuthContext } from "providers/authContext";
import Header from "app/Header"; import Header from "app/Header";
import Logout from "pages/Logout"; import Logout from "pages/Logout";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
@ -57,7 +57,7 @@ export const appendToUrl = (appendage: number | string) => {
export default function Router() { export default function Router() {
const { /*isAuthenticated, loginWithRedirect,*/ isLoading } = useAuth0(); const { isAuthenticated } = useAuthContext();
const whiteLabel = getWhitelabel(); const whiteLabel = getWhitelabel();
const [{ user }] = useGlobalState(); const [{ user }] = useGlobalState();
@ -306,13 +306,7 @@ export default function Router() {
); );
} }
if (isLoading) return null; if (!isAuthenticated) return null;
// if (!isAuthenticated) {
// loginWithRedirect()
// return (
// null
// )
// }
function ErrorFallback({ error }: { error: Error }) { function ErrorFallback({ error }: { error: Error }) {
return <div>Something went wrong: {error.stack}</div>; return <div>Something went wrong: {error.stack}</div>;

View file

@ -23,19 +23,9 @@ import BindaptIcon from "../products/Bindapt/BindaptIcon";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import BinsIcon from "products/Bindapt/BinsIcon"; import BinsIcon from "products/Bindapt/BinsIcon";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import { import { getFeatures } from "services/whiteLabel";
IsAdaptiveAgriculture,
// hasTutorialPlaylist,
IsAdCon,
IsIntellifarms,
IsMiPCA,
// isBXT,
IsMiVent,
IsOmniAir,
IsStreamline,
} from "services/whiteLabel";
import MiningIcon from "products/ventilation/MiningIcon"; import MiningIcon from "products/ventilation/MiningIcon";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuthContext } from "providers/authContext";
import FieldsIcon from "products/AgIcons/FieldsIcon"; import FieldsIcon from "products/AgIcons/FieldsIcon";
import PlaneIcon from "products/AviationIcons/PlaneIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon";
import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon";
@ -53,6 +43,7 @@ import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon"; import LibraCartIcon from "products/CommonIcons/libracartIcon";
const drawerWidth = 230; const drawerWidth = 230;
const closedDrawerWidth = 9.25;
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
sideMenu: { sideMenu: {
@ -66,6 +57,8 @@ const useStyles = makeStyles((theme: Theme) => ({
sideMenuOpened: { sideMenuOpened: {
zIndex: theme.zIndex.drawer + 2, zIndex: theme.zIndex.drawer + 2,
width: drawerWidth, width: drawerWidth,
minWidth: drawerWidth,
maxWidth: drawerWidth,
transition: theme.transitions.create(["width"], { transition: theme.transitions.create(["width"], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen duration: theme.transitions.duration.enteringScreen
@ -76,12 +69,16 @@ const useStyles = makeStyles((theme: Theme) => ({
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen duration: theme.transitions.duration.leavingScreen
}), }),
// overflowX: "hidden", overflowX: "hidden",
width: theme.spacing(0), width: theme.spacing(0),
minWidth: theme.spacing(0),
maxWidth: theme.spacing(0),
// zIndex: theme.zIndex.drawer, // zIndex: theme.zIndex.drawer,
// opacity: 0, // opacity: 0,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
width: theme.spacing(9.25), width: theme.spacing(closedDrawerWidth),
minWidth: theme.spacing(closedDrawerWidth),
maxWidth: theme.spacing(closedDrawerWidth),
opacity: 1 opacity: 1
} }
}, },
@ -130,7 +127,7 @@ interface Props {
export default function SideNavigator(props: Props) { export default function SideNavigator(props: Props) {
const { open, onOpen, onClose } = props; const { open, onOpen, onClose } = props;
const { isAuthenticated } = useAuth0() const { isAuthenticated } = useAuthContext()
const theme = useTheme(); const theme = useTheme();
const width = useWidth(); const width = useWidth();
const classes = useStyles(); const classes = useStyles();
@ -165,16 +162,10 @@ export default function SideNavigator(props: Props) {
} }
const authenticatedSideMenu = () => { const authenticatedSideMenu = () => {
const isMiVent = IsMiVent(); const f = getFeatures();
const isAg = IsAdaptiveAgriculture()
const isIntel = IsIntellifarms()
const isStreamline = IsStreamline()
const isOmni = IsOmniAir()
const isMiPCA = IsMiPCA()
const isAdCon = IsAdCon()
return ( return (
<List className={classes.list} component="nav"> <List className={classes.list} component="nav">
{(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( {(f.visualFarm || user.hasFeature("admin")) && (
<Tooltip title="Visual Farm" placement="right"> <Tooltip title="Visual Farm" placement="right">
<ListItemButton <ListItemButton
id="tour-visual-farm" id="tour-visual-farm"
@ -188,7 +179,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{((isOmni || isMiPCA) || user.hasFeature("admin")) && ( {(f.aviationMap || user.hasFeature("admin")) && (
<Tooltip title="Aviation Map" placement="right"> <Tooltip title="Aviation Map" placement="right">
<ListItemButton <ListItemButton
id="tour-aviation-map" id="tour-aviation-map"
@ -202,7 +193,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(isAdCon || user.hasFeature("admin")) && ( {(f.constructionMap || user.hasFeature("admin")) && (
<Tooltip title="Construction Map" placement="right"> <Tooltip title="Construction Map" placement="right">
<ListItemButton <ListItemButton
id="tour-construction-map" id="tour-construction-map"
@ -216,7 +207,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( {(f.contracts || user.hasFeature("admin")) && (
<Tooltip title="Contracts" placement="right"> <Tooltip title="Contracts" placement="right">
<ListItemButton <ListItemButton
id="tour-contracts" id="tour-contracts"
@ -230,7 +221,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( {(f.bins || user.hasFeature("admin")) && (
<Tooltip title="Bins" placement="right"> <Tooltip title="Bins" placement="right">
<ListItemButton <ListItemButton
id="tour-bins" id="tour-bins"
@ -244,7 +235,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{((isOmni || isMiPCA) || user.hasFeature("admin")) && ( {(f.terminals || user.hasFeature("admin")) && (
<Tooltip title="Terminals" placement="right"> <Tooltip title="Terminals" placement="right">
<ListItemButton <ListItemButton
id="tour-terminals" id="tour-terminals"
@ -258,7 +249,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(user.hasFeature("installer") && isAg || user.hasFeature("admin")) && {(f.cableEstimator && user.hasFeature("installer") || user.hasFeature("admin")) &&
<Tooltip title="Cable Estimator" placement="right"> <Tooltip title="Cable Estimator" placement="right">
<ListItemButton <ListItemButton
id="tour-cable-estimator" id="tour-cable-estimator"
@ -272,7 +263,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
} }
{(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( {(f.transactions || user.hasFeature("admin")) && (
<Tooltip title="Transactions" placement="right"> <Tooltip title="Transactions" placement="right">
<ListItemButton <ListItemButton
id="tour-transactions" id="tour-transactions"
@ -286,7 +277,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(isMiVent || user.hasFeature("admin")) && ( {(f.mines || user.hasFeature("admin")) && (
<Tooltip title="Mines" placement="right"> <Tooltip title="Mines" placement="right">
<ListItemButton <ListItemButton
id="tour-mines" id="tour-mines"
@ -364,7 +355,7 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Users" />} {open && <ListItemText primary="Users" />}
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
{(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( {(f.fields || user.hasFeature("admin")) && (
<Tooltip title="My Fields" placement="right"> <Tooltip title="My Fields" placement="right">
<ListItemButton <ListItemButton
id="tour-field-list" id="tour-field-list"
@ -378,7 +369,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( {(f.grainTypes || user.hasFeature("admin")) && (
<Tooltip title="Grain Types" placement="right"> <Tooltip title="Grain Types" placement="right">
<ListItemButton <ListItemButton
id="tour-grain-types" id="tour-grain-types"
@ -393,7 +384,7 @@ export default function SideNavigator(props: Props) {
</Tooltip> </Tooltip>
)} )}
{(isAdCon || user.hasFeature("admin")) && ( {(f.jobsites || user.hasFeature("admin")) && (
<Tooltip title="Jobsites" placement="right"> <Tooltip title="Jobsites" placement="right">
<ListItemButton <ListItemButton
id="tour-jobsites" id="tour-jobsites"
@ -407,7 +398,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
)} )}
{(isAdCon || user.hasFeature("admin")) && ( {(f.heaters || user.hasFeature("admin")) && (
<Tooltip title="Heaters" placement="right"> <Tooltip title="Heaters" placement="right">
<ListItemButton <ListItemButton
id="tour-heaters" id="tour-heaters"
@ -462,7 +453,7 @@ export default function SideNavigator(props: Props) {
</ListItemButton> </ListItemButton>
</Tooltip> </Tooltip>
} }
{(isAg || isIntel || isStreamline || user.hasFeature("admin")) && {(f.marketplace || user.hasFeature("admin")) &&
<Tooltip title="Marketplace" placement="right"> <Tooltip title="Marketplace" placement="right">
<ListItemButton <ListItemButton
id="tour-marketplace" id="tour-marketplace"
@ -538,11 +529,16 @@ export default function SideNavigator(props: Props) {
onClose={onClose} onClose={onClose}
sx={{ pointerEvents: isMobile&&!open ? "none" : "auto"}} sx={{ pointerEvents: isMobile&&!open ? "none" : "auto"}}
> >
<Toolbar > <Toolbar>
<Grid container direction="row" justifyContent={"flex-end"}> <Grid container direction="row" justifyContent={"flex-end"}>
<Grid> <Grid>
<IconButton onClick={onClose} aria-label="onClose side menu"> <IconButton
{theme.direction === "rtl" ? <ChevronRight /> : <ChevronLeft />} onClick={open ? onClose : onOpen}
aria-label={open ? "Close side menu" : "Open side menu"}
>
{open
? theme.direction === "rtl" ? <ChevronRight /> : <ChevronLeft />
: theme.direction === "rtl" ? <ChevronLeft /> : <ChevronRight />}
</IconButton> </IconButton>
</Grid> </Grid>
</Grid> </Grid>

View file

@ -1,10 +1,11 @@
import { RedirectLoginOptions, useAuth0 } from "@auth0/auth0-react"; import { RedirectLoginOptions } from "@auth0/auth0-react";
// import { useAuth } from "hooks"; // import { useAuth } from "hooks";
import queryString from "query-string"; import queryString from "query-string";
import { useEffect } from "react"; import { useEffect } from "react";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
// import Loading from "./Loading"; // import Loading from "./Loading";
import LoadingScreen from "app/LoadingScreen"; import LoadingScreen from "app/LoadingScreen";
import { useAuthContext } from "providers/authContext";
// interface Props { // interface Props {
// prevPath?: string; // prevPath?: string;
@ -13,7 +14,7 @@ import LoadingScreen from "app/LoadingScreen";
export default function Login() { export default function Login() {
// const { prevPath } = props; // const { prevPath } = props;
const location = useLocation(); const location = useLocation();
const { loginWithRedirect } = useAuth0(); const { loginWithRedirect } = useAuthContext();
// const setRouteBeforeLogin = useCallback((): Promise<string> => { // const setRouteBeforeLogin = useCallback((): Promise<string> => {
// return new Promise(function(resolve) { // return new Promise(function(resolve) {

View file

@ -1,9 +1,9 @@
import { useAuth0 } from "@auth0/auth0-react"; import { useAuthContext } from "providers/authContext";
import LoadingScreen from "app/LoadingScreen"; import LoadingScreen from "app/LoadingScreen";
import { useEffect } from "react"; import { useEffect } from "react";
export default function Logout() { export default function Logout() {
const { logout } = useAuth0(); const { logout } = useAuthContext();
useEffect(() => { useEffect(() => {
logout(); logout();

View file

@ -7,7 +7,7 @@ import { CheckCircleOutline } from "@mui/icons-material";
import { green } from "@mui/material/colors"; import { green } from "@mui/material/colors";
import Tour, { TourStep } from "common/Tour"; import Tour, { TourStep } from "common/Tour";
import Emoji from "react-emoji-render"; import Emoji from "react-emoji-render";
import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel"; import { getWhitelabel, getFeatures } from "services/whiteLabel";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { User } from "models"; import { User } from "models";
// import { color } from "framer-motion"; // import { color } from "framer-motion";
@ -154,7 +154,7 @@ export default function SignupCallback () {
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_FEET}>Feet (ft)</MenuItem> <MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_FEET}>Feet (ft)</MenuItem>
</TextField> </TextField>
</Grid2> </Grid2>
{IsAdaptiveAgriculture() && ( {getFeatures().grainUnit && (
<Grid2 size={{ xs: 12 }}> <Grid2 size={{ xs: 12 }}>
<TextField <TextField
select select
@ -294,7 +294,7 @@ export default function SignupCallback () {
placement: "right" placement: "right"
}) })
//tasks step //tasks step
if (IsAdaptiveAgriculture()) { if (getFeatures().visualFarm) {
steps.push({ steps.push({
title: "Visual Farm", title: "Visual Farm",
content: ( content: (

View file

@ -16,7 +16,7 @@ import { useGlobalState, useSnackbar, useUserAPI } from "providers";
import { useTeamAPI } from "providers/pond/teamAPI"; import { useTeamAPI } from "providers/pond/teamAPI";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { getSignatureAccentColour, IsAdaptiveAgriculture, IsStreamline } from "services/whiteLabel"; import { getSignatureAccentColour, getFeatures } from "services/whiteLabel";
import TeamActions from "teams/TeamActions"; import TeamActions from "teams/TeamActions";
import TeamKeyManager from "teams/TeamKeyManager"; import TeamKeyManager from "teams/TeamKeyManager";
import ObjectUsers from "user/ObjectUsers"; import ObjectUsers from "user/ObjectUsers";
@ -220,8 +220,7 @@ export default function TeamPage() {
navigate("bins") navigate("bins")
} }
const isAg = IsAdaptiveAgriculture() const f = getFeatures()
const isStreamline = IsStreamline()
return ( return (
<PageContainer spacing={isMobile ? 1 : 2}> <PageContainer spacing={isMobile ? 1 : 2}>
@ -267,7 +266,7 @@ export default function TeamPage() {
<ListItemButton onClick={toGroups}> <ListItemButton onClick={toGroups}>
Groups Groups
</ListItemButton> </ListItemButton>
{((isAg || isStreamline) || user.hasFeature("admin")) && {(f.bins || user.hasFeature("admin")) &&
<> <>
<Divider /> <Divider />
<ListItemButton onClick={toBins}> <ListItemButton onClick={toBins}>

View file

@ -1,7 +1,7 @@
import { useAuth0 } from '@auth0/auth0-react'; import { useAuthContext } from './authContext';
const LoginButton = () => { const LoginButton = () => {
const { loginWithRedirect } = useAuth0(); const { loginWithRedirect } = useAuthContext();
return ( return (
<button onClick={() => loginWithRedirect()}> <button onClick={() => loginWithRedirect()}>

View file

@ -0,0 +1,44 @@
import { RedirectLoginOptions, useAuth0 } from "@auth0/auth0-react"
import { createContext, PropsWithChildren, useContext } from "react"
interface IAuthContext {
isAuthenticated: boolean
loginWithRedirect: (options?: RedirectLoginOptions) => void | Promise<void>
logout: () => void
}
const AuthContext = createContext<IAuthContext>({
isAuthenticated: false,
loginWithRedirect: () => {},
logout: () => {},
})
export function LocalAuthProvider(props: PropsWithChildren<{ token: string }>) {
const { children, token } = props
const doLogout = () => {
localStorage.removeItem('local_auth_token')
window.location.href = '/'
}
return (
<AuthContext.Provider
value={{
isAuthenticated: !!token,
loginWithRedirect: doLogout,
logout: doLogout,
}}
>
{children}
</AuthContext.Provider>
)
}
export function Auth0AuthBridge(props: PropsWithChildren<{}>) {
const { isAuthenticated, loginWithRedirect, logout } = useAuth0()
return (
<AuthContext.Provider value={{ isAuthenticated, loginWithRedirect, logout: () => logout({ logoutParams: { returnTo: window.location.origin } }) }}>
{props.children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => useContext(AuthContext)

View file

@ -2,7 +2,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import moment from "moment"; import moment from "moment";
import { createContext, PropsWithChildren, useContext } from "react"; import { createContext, PropsWithChildren, useContext } from "react";
import PondProvider from "./pond/pond"; import PondProvider from "./pond/pond";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuthContext } from "./authContext";
import SnackbarProvider from "./Snackbar"; import SnackbarProvider from "./Snackbar";
interface IHTTPContext { interface IHTTPContext {
@ -30,7 +30,7 @@ export const HTTPContext = createContext<IHTTPContext>({} as IHTTPContext);
export default function HTTPProvider(props: Props) { export default function HTTPProvider(props: Props) {
const { children, token } = props; const { children, token } = props;
const { isAuthenticated, loginWithRedirect } = useAuth0(); const { isAuthenticated, loginWithRedirect } = useAuthContext();
const defaultOptions = (demo: boolean = false) => { const defaultOptions = (demo: boolean = false) => {
if (demo || !isAuthenticated || !token) { if (demo || !isAuthenticated || !token) {
@ -50,31 +50,24 @@ export default function HTTPProvider(props: Props) {
return config; return config;
}; };
function isTokenExpired(token: string): boolean { function isTokenExpired(token: string | undefined): boolean {
if (!token) return true;
try { try {
// Split the token and decode the payload (second part)
const payloadBase64 = token.split('.')[1]; const payloadBase64 = token.split('.')[1];
const decodedPayload = atob(payloadBase64); // Decode base64 const payload = JSON.parse(atob(payloadBase64));
const payload = JSON.parse(decodedPayload);
// Get expiration time (in seconds)
const exp = payload.exp; const exp = payload.exp;
if (!exp) return true;
if (!exp) return true; // No exp field? Treat as expired return Math.floor(Date.now() / 1000) >= exp;
} catch {
// Current time in seconds return true;
const now = Math.floor(Date.now() / 1000);
// Compare
return now >= exp;
} catch (error) {
console.error("Invalid token format:", error);
return true; // Err on the side of caution if decoding fails
} }
} }
function get<T>(url: string, spreadOptions?: AxiosRequestConfig): Promise<AxiosResponse<T>> { function get<T>(url: string, spreadOptions?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
if (isTokenExpired(token)) loginWithRedirect() if (isTokenExpired(token)) {
loginWithRedirect();
return Promise.reject(new Error("token expired"));
}
return axios.get(url, {...defaultOptions(), ...spreadOptions}); return axios.get(url, {...defaultOptions(), ...spreadOptions});
} }
@ -83,6 +76,10 @@ export default function HTTPProvider(props: Props) {
data?: any, data?: any,
spreadOptions?: AxiosRequestConfig spreadOptions?: AxiosRequestConfig
): Promise<AxiosResponse<T>> { ): Promise<AxiosResponse<T>> {
if (isTokenExpired(token)) {
loginWithRedirect();
return Promise.reject(new Error("token expired"));
}
return axios.put(url, data, { ...defaultOptions(), ...spreadOptions }); return axios.put(url, data, { ...defaultOptions(), ...spreadOptions });
} }
@ -91,10 +88,18 @@ export default function HTTPProvider(props: Props) {
data?: any, data?: any,
spreadOptions?: AxiosRequestConfig spreadOptions?: AxiosRequestConfig
): Promise<AxiosResponse<T>> { ): Promise<AxiosResponse<T>> {
if (isTokenExpired(token)) {
loginWithRedirect();
return Promise.reject(new Error("token expired"));
}
return axios.post(url, data, { ...defaultOptions(), ...spreadOptions }); return axios.post(url, data, { ...defaultOptions(), ...spreadOptions });
} }
function del<T>(url: string, spreadOptions?: AxiosRequestConfig): Promise<AxiosResponse<T>> { function del<T>(url: string, spreadOptions?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
if (isTokenExpired(token)) {
loginWithRedirect();
return Promise.reject(new Error("token expired"));
}
return axios.delete(url, { ...defaultOptions(), ...spreadOptions }); return axios.delete(url, { ...defaultOptions(), ...spreadOptions });
} }

View file

@ -1,4 +1,3 @@
// import { isOffline } from "utils/environment";
import DefaultDarkLogo from "../assets/whitelabels/darkLogo.png"; import DefaultDarkLogo from "../assets/whitelabels/darkLogo.png";
import DefaultLightLogo from "../assets/whitelabels/lightLogo.png"; import DefaultLightLogo from "../assets/whitelabels/lightLogo.png";
import AdapativeAgLogo from "../assets/whitelabels/AdaptiveAgriculture/logo.png"; import AdapativeAgLogo from "../assets/whitelabels/AdaptiveAgriculture/logo.png";
@ -9,12 +8,10 @@ import BXTDarkLogo from "../assets/whitelabels/BXT/darkLogo.png";
import AeroGrowDarkLogo from "../assets/whitelabels/AeroGrow/darkLogo.png"; import AeroGrowDarkLogo from "../assets/whitelabels/AeroGrow/darkLogo.png";
import AeroGrowLightLogo from "../assets/whitelabels/AeroGrow/lightLogo.png"; import AeroGrowLightLogo from "../assets/whitelabels/AeroGrow/lightLogo.png";
import MiVentLightLogo from "../assets/whitelabels/MiVent/lightLogo.png"; import MiVentLightLogo from "../assets/whitelabels/MiVent/lightLogo.png";
// import OmniAirLogo from "../assets/whitelabels/OmniAir/OmniAirLogo.png";
import MiPCALogo from "../assets/whitelabels/MiPCA/MiPCALogo.png"; import MiPCALogo from "../assets/whitelabels/MiPCA/MiPCALogo.png";
import StreamlineLogo from "../assets/whitelabels/Streamline/stream-logo.png" import StreamlineLogo from "../assets/whitelabels/Streamline/stream-logo.png"
import IntellifarmsLogo from "../assets/whitelabels/Intellifarms/IFND-2023-Logo.png" import IntellifarmsLogo from "../assets/whitelabels/Intellifarms/IFND-2023-Logo.png"
import IntellifarmsLogoWhite from "../assets/whitelabels/Intellifarms/IFND-2023-Logo-White.png" import IntellifarmsLogoWhite from "../assets/whitelabels/Intellifarms/IFND-2023-Logo-White.png"
// import { green, yellow } from "@mui/material/colors";
const protips: string[] = [ const protips: string[] = [
"You can see the latest measurements for a device by starring its components!", "You can see the latest measurements for a device by starring its components!",
@ -31,354 +28,408 @@ const protips: string[] = [
"Want to receive text messages about your devices? Update your phone number and enable SMS notifications!" "Want to receive text messages about your devices? Update your phone number and enable SMS notifications!"
]; ];
export interface WhiteLabelFeatures {
bins: boolean;
visualFarm: boolean;
contracts: boolean;
transactions: boolean;
fields: boolean;
grainTypes: boolean;
grainUnit: boolean;
marketplace: boolean;
cableEstimator: boolean;
ventilation: boolean;
mines: boolean;
constructionMap: boolean;
jobsites: boolean;
heaters: boolean;
aviationMap: boolean;
terminals: boolean;
security: boolean;
}
interface WhiteLabel { interface WhiteLabel {
name: any; slug: string;
primaryColour: any; //must be a MATERIAL UI colour, used for various UI elements name: string;
secondaryColour: any; //must be a MATERIAL UI colour, used for a few UI elements (less importance) primaryColour: any;
signatureColour: any; //hex or RGB secondaryColour: any;
signatureAccentColour: any; //hex or RGB signatureColour: any;
auth0ClientId: any; signatureAccentColour: any;
auth0ClientId: string;
redirectOnLogout: boolean; redirectOnLogout: boolean;
logoutRedirectTarget: string; logoutRedirectTarget: string;
darkLogo: any; darkLogo: any;
lightLogo: any; lightLogo: any;
transparentLogoBG: boolean; //determines whether the background of the logo should be transparent or not transparentLogoBG: boolean;
blacklist: string[]; blacklist: string[];
features: WhiteLabelFeatures;
hotjarID?: string; hotjarID?: string;
docs: string; docs: string;
protips: string[]; protips: string[];
tutorialPlaylistID?: string; tutorialPlaylistID?: string;
thumbnail?: string; thumbnail?: string;
tutorialFiles?: { name: string; url: string }[]; tutorialFiles?: { name: string; url: string }[];
homePage?: string; homePage: string;
} }
// const DEFAULT_WHITELABEL: WhiteLabel = { const AG_FEATURES: WhiteLabelFeatures = {
// name: import.meta.env.REACT_APP_WEBSITE_TITLE, bins: true,
// primaryColour: import.meta.env.REACT_APP_PRIMARY_COLOUR, visualFarm: true,
// secondaryColour: import.meta.env.REACT_APP_SECONDARY_COLOUR, contracts: true,
// signatureColour: import.meta.env.REACT_APP_SIGNATURE_COLOUR, transactions: true,
// signatureAccentColour: "#fff", fields: true,
// auth0ClientId: import.meta.env.REACT_APP_AUTH0_CLIENT_ID, grainTypes: true,
// redirectOnLogout: false, grainUnit: true,
// logoutRedirectTarget: "", marketplace: true,
// darkLogo: DefaultDarkLogo, cableEstimator: true,
// lightLogo: DefaultLightLogo, ventilation: false,
// transparentLogoBG: false, mines: false,
// blacklist: [], constructionMap: false,
// hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_BXT, jobsites: false,
// docs: "Platform", heaters: false,
// protips: protips, aviationMap: false,
// tutorialPlaylistID: "" terminals: false,
// }; security: false,
const STAGING_WHITELABEL: WhiteLabel = {
name: "Staging",
primaryColour: import.meta.env.VITE_APP_PRIMARY_COLOUR,
secondaryColour: import.meta.env.VITE_APP_SECONDARY_COLOUR,
signatureColour: import.meta.env.VITE_APP_SIGNATURE_COLOUR,
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_STAGING_CLIENT_ID,
redirectOnLogout: false,
logoutRedirectTarget: "",
darkLogo: DefaultDarkLogo,
lightLogo: DefaultLightLogo,
transparentLogoBG: false,
blacklist: [],
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_BXT,
docs: "Platform",
protips: protips,
tutorialPlaylistID: "",
homePage: "/devices"
}; };
const BXT_WHITE_LABEL: WhiteLabel = { const CONSTRUCTION_FEATURES: WhiteLabelFeatures = {
name: "Brand X Technologies", bins: false,
primaryColour: "blue", visualFarm: false,
// primaryColour: "#0000FF", contracts: false,
secondaryColour: "yellow", transactions: false,
// secondaryColour: "#FFFF00", fields: false,
// signatureColour: "#272727", grainTypes: false,
signatureColour: "#005bb0", grainUnit: false,
signatureAccentColour: "#fff", marketplace: false,
auth0ClientId: import.meta.env.VITE_AUTH0_DEV_CLIENT_ID, cableEstimator: false,
redirectOnLogout: false, ventilation: false,
logoutRedirectTarget: "", mines: false,
darkLogo: BXTDarkLogo, constructionMap: true,
lightLogo: BXTLightLogo, jobsites: true,
transparentLogoBG: false, heaters: true,
blacklist: [], aviationMap: false,
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_BXT, terminals: false,
docs: "Platform", security: false,
protips: protips,
homePage: "/devices"
}; };
const STREAMLINE_WHITE_LABEL: WhiteLabel = { const AVIATION_FEATURES: WhiteLabelFeatures = {
name: "Streamline", bins: false,
primaryColour: "#FFFF", visualFarm: false,
// primaryColour: "#0000FF", contracts: false,
secondaryColour: "yellow", transactions: false,
// secondaryColour: "#FFFF00", fields: false,
// signatureColour: "#272727", grainTypes: false,
signatureColour: "#005bb0", grainUnit: false,
signatureAccentColour: "#fff", marketplace: false,
auth0ClientId: import.meta.env.VITE_AUTH0_STREAMLINE_CLIENT_ID, cableEstimator: false,
redirectOnLogout: false, ventilation: false,
logoutRedirectTarget: "", mines: false,
darkLogo: StreamlineLogo, constructionMap: false,
lightLogo: StreamlineLogo, jobsites: false,
transparentLogoBG: false, heaters: false,
blacklist: [], aviationMap: true,
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_BXT, terminals: true,
docs: "Platform", security: false,
protips: protips,
homePage: "/devices"
}; };
export function isBXT(): boolean { const MIVENT_FEATURES: WhiteLabelFeatures = {
return getName() === "Brand X Technologies"; bins: false,
visualFarm: false,
contracts: false,
transactions: false,
fields: false,
grainTypes: false,
grainUnit: false,
marketplace: false,
cableEstimator: false,
ventilation: true,
mines: true,
constructionMap: false,
jobsites: false,
heaters: false,
aviationMap: false,
terminals: false,
security: false,
};
const BASE_FEATURES: WhiteLabelFeatures = {
bins: false,
visualFarm: false,
contracts: false,
transactions: false,
fields: false,
grainTypes: false,
grainUnit: false,
marketplace: false,
cableEstimator: false,
ventilation: false,
mines: false,
constructionMap: false,
jobsites: false,
heaters: false,
aviationMap: false,
terminals: false,
security: false,
};
const registry: Record<string, WhiteLabel> = {
"bxt": {
slug: "bxt",
name: "Brand X Technologies",
primaryColour: "blue",
secondaryColour: "yellow",
signatureColour: "#005bb0",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_DEV_CLIENT_ID,
redirectOnLogout: false,
logoutRedirectTarget: "",
darkLogo: BXTDarkLogo,
lightLogo: BXTLightLogo,
transparentLogoBG: false,
blacklist: [],
features: { ...BASE_FEATURES, security: true },
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_BXT,
docs: "Platform",
protips: protips,
homePage: "/devices",
},
"staging": {
slug: "staging",
name: "Staging",
primaryColour: import.meta.env.VITE_APP_PRIMARY_COLOUR,
secondaryColour: import.meta.env.VITE_APP_SECONDARY_COLOUR,
signatureColour: import.meta.env.VITE_APP_SIGNATURE_COLOUR,
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_STAGING_CLIENT_ID,
redirectOnLogout: false,
logoutRedirectTarget: "",
darkLogo: DefaultDarkLogo,
lightLogo: DefaultLightLogo,
transparentLogoBG: false,
blacklist: [],
features: AG_FEATURES,
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_BXT,
docs: "Platform",
protips: protips,
homePage: "/devices",
},
"adaptive-ag": {
slug: "adaptive-ag",
name: "Adaptive Agriculture",
primaryColour: "green",
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://adaptiveagriculture.ca",
darkLogo: AdapativeAgLogo,
lightLogo: AdapativeAgLogo,
transparentLogoBG: true,
blacklist: ["cost"],
features: AG_FEATURES,
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_ADAPTIVE_AGRICULTURE,
docs: "AdaptiveAg",
protips: protips,
tutorialPlaylistID: "PLpLmJnI66Jfl5FXME31ckGam-sD8gB1s2",
thumbnail: AdaptiveAgThumbnail,
tutorialFiles: [
{
name: "Bindapt+",
url: "https://adaptiveagriculture.ca/wp-content/uploads/2023/08/Bindapt-Set-Up-Guide.pdf"
},
{
name: "Adapter Plate",
url: "https://adaptiveagriculture.ca/wp-content/uploads/2023/08/Bindapt-Adapter-Plate-Set-Up-Guide.pdf"
}
],
homePage: "/bins",
},
"intellifarms": {
slug: "intellifarms",
name: "Intellifarms",
primaryColour: "#9E1B32",
secondaryColour: "#4A4F55",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_INTELLIFARMS_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://myintellifarms.com",
darkLogo: IntellifarmsLogo,
lightLogo: IntellifarmsLogoWhite,
transparentLogoBG: true,
blacklist: [],
features: AG_FEATURES,
docs: "Platform",
protips: protips,
homePage: "/bins",
},
"streamline": {
slug: "streamline",
name: "Streamline",
primaryColour: "#FFFF",
secondaryColour: "yellow",
signatureColour: "#005bb0",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_STREAMLINE_CLIENT_ID,
redirectOnLogout: false,
logoutRedirectTarget: "",
darkLogo: StreamlineLogo,
lightLogo: StreamlineLogo,
transparentLogoBG: false,
blacklist: [],
features: AG_FEATURES,
docs: "Platform",
protips: protips,
homePage: "/devices",
},
"aerogrow": {
slug: "aerogrow",
name: "AeroGrow",
primaryColour: "green",
secondaryColour: "cyan",
signatureColour: "#fff",
signatureAccentColour: "#000",
auth0ClientId: import.meta.env.VITE_AUTH0_AEROGROW_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://www.aerogrowmanufacturing.com",
darkLogo: AeroGrowDarkLogo,
lightLogo: AeroGrowLightLogo,
transparentLogoBG: true,
blacklist: [],
features: BASE_FEATURES,
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_AEROGROW,
docs: "Platform",
protips: protips,
homePage: "/devices",
},
"mivent": {
slug: "mivent",
name: "MiVent",
primaryColour: "green",
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_MIVENT_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://mivent.ca",
darkLogo: MiVentLightLogo,
lightLogo: MiVentLightLogo,
transparentLogoBG: true,
blacklist: ["cost"],
features: MIVENT_FEATURES,
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_MIVENT,
docs: "Platform",
protips: protips,
homePage: "/devices",
},
"adaptive-construction": {
slug: "adaptive-construction",
name: "Adaptive Construction",
primaryColour: "blue",
secondaryColour: "blue",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_ADAPTIVE_CONSTRUCTION_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://adaptiveconstruction.ca",
darkLogo: AdConLogo,
lightLogo: AdConLogo,
transparentLogoBG: true,
blacklist: ["cost"],
features: CONSTRUCTION_FEATURES,
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_ADAPTIVE_CONSTRUCTION,
docs: "AdaptiveConstruction",
protips: protips,
homePage: "/devices",
},
"omniair": {
slug: "omniair",
name: "OmniAir",
primaryColour: "#004f9b",
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_OMNIAIR_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://omniairsystems.com",
darkLogo: MiPCALogo,
lightLogo: MiPCALogo,
transparentLogoBG: true,
blacklist: ["cost"],
features: AVIATION_FEATURES,
docs: "OmniAir",
protips: protips,
homePage: "/devices",
},
"mipca": {
slug: "mipca",
name: "MiPCA",
primaryColour: "#004f9b",
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_OMNIAIR_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://mionetech.com",
darkLogo: MiPCALogo,
lightLogo: MiPCALogo,
transparentLogoBG: true,
blacklist: ["cost"],
features: AVIATION_FEATURES,
docs: "MiPCA",
protips: protips,
homePage: "/devices",
},
};
// Ordered most-specific to least-specific
const DOMAIN_RULES: [RegExp, string][] = [
[/bxt-dev/, "bxt"],
[/staging\.brandxtech/, "staging"],
[/streamline\./, "streamline"],
[/brandxtech|brandxducks/, "bxt"],
[/adaptiveagriculture|adaptiveag/, "adaptive-ag"],
[/adaptiveconstruction/, "adaptive-construction"],
[/aerogrowmanufacturing/, "aerogrow"],
[/mivent/, "mivent"],
[/omniair/, "omniair"],
[/mionetech|mipca/, "mipca"],
[/myintellifarms|intellifarms/, "intellifarms"],
];
function resolveSlug(): string {
const override = import.meta.env.VITE_WHITELABEL;
if (override && registry[override]) return override;
const host = window.location.hostname;
for (const [pattern, slug] of DOMAIN_RULES) {
if (pattern.test(host)) return slug;
}
return "adaptive-ag";
} }
const ADAPTIVE_AGRICULTURE_WHITE_LABEL: WhiteLabel = { let cached: WhiteLabel | null = null;
name: "Adaptive Agriculture",
primaryColour: "green",
// primaryColour: "#008000",
secondaryColour: "yellow",
// secondaryColour: "#FFFF00",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://adaptiveagriculture.ca",
darkLogo: AdapativeAgLogo,
lightLogo: AdapativeAgLogo,
transparentLogoBG: true,
blacklist: ["cost"],
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_ADAPTIVE_AGRICULTURE,
docs: "AdaptiveAg",
protips: protips.concat([]),
tutorialPlaylistID: "PLpLmJnI66Jfl5FXME31ckGam-sD8gB1s2",
thumbnail: AdaptiveAgThumbnail,
tutorialFiles: [
{
name: "Bindapt+",
url: "https://adaptiveagriculture.ca/wp-content/uploads/2023/08/Bindapt-Set-Up-Guide.pdf"
},
{
name: "Adapter Plate",
url:
"https://adaptiveagriculture.ca/wp-content/uploads/2023/08/Bindapt-Adapter-Plate-Set-Up-Guide.pdf"
}
],
homePage: "/bins"
};
export function IsAdaptiveAgriculture(): boolean {
return (
getName() === "Adaptive Agriculture" ||
window.location.origin.includes("staging") ||
window.location.origin.includes("localhost")
);
}
const INTELLIFARMS_WHITE_LABEL: WhiteLabel = {
name: "Intellifarms",
primaryColour: "#9E1B32",
secondaryColour: "#4A4F55",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_INTELLIFARMS_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://myintellifarms.com",
darkLogo: IntellifarmsLogo,
lightLogo: IntellifarmsLogoWhite,
transparentLogoBG: true,
blacklist: [],
docs: "Platform",
protips: protips,
homePage: "/bins"
};
export function IsIntellifarms(): boolean {
return (
getName() === "Intellifarms"
// window.location.origin.includes("staging") ||
// window.location.origin.includes("localhost")
);
}
export function IsStreamline(): boolean {
return (
getName() === "Streamline"
// window.location.origin.includes("staging") ||
// window.location.origin.includes("localhost")
);
}
const AEROGROW_WHITE_LABEL: WhiteLabel = {
name: "AeroGrow",
primaryColour: "green",
secondaryColour: "cyan",
signatureColour: "#fff",
signatureAccentColour: "#000",
auth0ClientId: import.meta.env.VITE_AUTH0_AEROGROW_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://www.aerogrowmanufacturing.com",
darkLogo: AeroGrowDarkLogo,
lightLogo: AeroGrowLightLogo,
transparentLogoBG: true,
blacklist: [],
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_AEROGROW,
docs: "Platform",
protips: protips
};
export function IsMiVent(): boolean {
return (
getName() === "MiVent" ||
window.location.origin.includes("staging") ||
window.location.origin.includes("localhost")
);
}
const MIVENT_WHITE_LABEL: WhiteLabel = {
name: "MiVent",
primaryColour: "green",
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_MIVENT_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://mivent.ca",
darkLogo: MiVentLightLogo,
lightLogo: MiVentLightLogo,
transparentLogoBG: true,
blacklist: ["cost"],
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_MIVENT,
docs: "Platform",
protips: protips.concat([])
};
const ADAPTIVE_CONSTRUCTION_WHITE_LABEL: WhiteLabel = {
name: "Adaptive Construction",
primaryColour: "blue",
secondaryColour: "blue",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_ADAPTIVE_CONSTRUCTION_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://adaptiveconstruction.ca",
darkLogo: AdConLogo,
lightLogo: AdConLogo,
transparentLogoBG: true,
blacklist: ["cost"],
hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_ADAPTIVE_CONSTRUCTION,
docs: "AdaptiveConstruction",
protips: protips.concat([])
};
export function IsAdCon(): boolean {
return (
getName() === "Adaptive Construction" ||
window.location.origin.includes("staging") ||
window.location.origin.includes("localhost")
);
}
const OMNIAIR_WHITE_LABEL: WhiteLabel = {
name: "OmniAir",
primaryColour: "#004f9b",
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.VITE_AUTH0_OMNIAIR_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://omniairsystems.com",
darkLogo: MiPCALogo,
lightLogo: MiPCALogo,
transparentLogoBG: true,
blacklist: ["cost"],
//hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_OMNIAIR, testing what happens if this is excluded
docs: "OmniAir",
protips: protips.concat([])
};
export function IsOmniAir(): boolean {
return (
getName() === "OmniAir" ||
window.location.origin.includes("staging") ||
window.location.origin.includes("localhost")
);
}
const MIPCA_WHITE_LABEL: WhiteLabel = {
name: "MiPCA",
primaryColour: "#004f9b",
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
//omni air and MiPCA are the same client ID in Auth0, it is to replace it, once omniair gets removed we can re-name this
auth0ClientId: import.meta.env.VITE_AUTH0_OMNIAIR_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://mionetech.com",
darkLogo: MiPCALogo,
lightLogo: MiPCALogo,
transparentLogoBG: true,
blacklist: ["cost"],
docs: "MiPCA",
protips: protips.concat([])
};
export function IsMiPCA(): boolean {
return (
getName() === "MiPCA" ||
window.location.origin.includes("staging") ||
window.location.origin.includes("localhost")
);
}
const whitelabels = new Map<string, WhiteLabel>([
["streamline", STREAMLINE_WHITE_LABEL],
["adaptiveag", ADAPTIVE_AGRICULTURE_WHITE_LABEL],
["adaptiveagriculture", ADAPTIVE_AGRICULTURE_WHITE_LABEL],
["brandxducks", BXT_WHITE_LABEL],
["brandxtech", BXT_WHITE_LABEL],
["aerogrowmanufacturing", AEROGROW_WHITE_LABEL],
["localhost", BXT_WHITE_LABEL],
["staging", STAGING_WHITELABEL],
["10.0", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
["mivent", MIVENT_WHITE_LABEL],
["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
["omniair", OMNIAIR_WHITE_LABEL],
["mipca", MIPCA_WHITE_LABEL],
["mionetech", MIPCA_WHITE_LABEL],
["intellifarms", INTELLIFARMS_WHITE_LABEL]
]);
export function getWhitelabel(): WhiteLabel { export function getWhitelabel(): WhiteLabel {
// if (isOffline()) { if (!cached) {
// return DEFAULT_WHITELABEL; cached = registry[resolveSlug()];
// } }
return cached;
}
const hostname = window.location.hostname; export function getFeatures(): WhiteLabelFeatures {
if (window.location.origin.includes("bxt-dev")) { return getWhitelabel().features;
return BXT_WHITE_LABEL;
}
if (window.location.origin.includes("localhost")) {
return INTELLIFARMS_WHITE_LABEL;
}
if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') {
return STAGING_WHITELABEL;
}
const whiteLabelKeys = Array.from(whitelabels.keys());
for (var i = 0; i < whiteLabelKeys.length; i++) {
let key = whiteLabelKeys[i];
if (hostname.includes(key)) {
return whitelabels.get(key) as WhiteLabel;
}
}
return ADAPTIVE_AGRICULTURE_WHITE_LABEL;
} }
export function getPrimaryColour(): any { export function getPrimaryColour(): any {
@ -397,7 +448,7 @@ export function getSignatureAccentColour(): any {
return getWhitelabel().signatureAccentColour; return getWhitelabel().signatureAccentColour;
} }
export function getAuth0ClientId(): any { export function getAuth0ClientId(): string {
return getWhitelabel().auth0ClientId; return getWhitelabel().auth0ClientId;
} }
@ -418,10 +469,6 @@ export function getLightLogo(): any {
} }
export function hideLogo(): boolean { export function hideLogo(): boolean {
// if (isOffline()) {
// return false;
// }
return getWhitelabel().name === ""; return getWhitelabel().name === "";
} }

View file

@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles";
import { LockOpen, Person, Lock, Settings, SupervisedUserCircle as TeamIcon, ExitToApp, PersonAdd } from "@mui/icons-material"; import { LockOpen, Person, Lock, Settings, SupervisedUserCircle as TeamIcon, ExitToApp, PersonAdd } from "@mui/icons-material";
import { useState } from "react"; import { useState } from "react";
import UserTeamName from "./UserTeamName"; import UserTeamName from "./UserTeamName";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuthContext } from "providers/authContext";
import UserSettings from "./UserSettings"; import UserSettings from "./UserSettings";
import UserAvatar from "./UserAvatar"; import UserAvatar from "./UserAvatar";
import { purple } from "@mui/material/colors"; import { purple } from "@mui/material/colors";
@ -77,7 +77,7 @@ export default function UserMenu() {
// const { toggleMode } = useThemeMode() // const { toggleMode } = useThemeMode()
const [{ user, team, as }, dispatch] = useGlobalState(); const [{ user, team, as }, dispatch] = useGlobalState();
const { loginWithRedirect } = useAuth0(); const { loginWithRedirect } = useAuthContext();
const classes = useStyles(); const classes = useStyles();
const theme = useTheme(); const theme = useTheme();

View file

@ -29,7 +29,7 @@ import { useThemeMode } from "theme/AppThemeProvider";
// import ContractsIcon from "products/CommonIcons/contractIcon"; // import ContractsIcon from "products/CommonIcons/contractIcon";
import UnitsIcon from "@mui/icons-material/Category"; import UnitsIcon from "@mui/icons-material/Category";
import { setThemeType } from "theme"; import { setThemeType } from "theme";
import { IsAdaptiveAgriculture } from "services/whiteLabel"; import { getFeatures } from "services/whiteLabel";
import { setDistanceUnit, setGrainUnit, setPressureUnit, setTemperatureUnit } from "utils"; import { setDistanceUnit, setGrainUnit, setPressureUnit, setTemperatureUnit } from "utils";
import FieldsIcon from "products/AgIcons/FieldsIcon"; import FieldsIcon from "products/AgIcons/FieldsIcon";
@ -529,7 +529,7 @@ export default function UserSettings(props: Props) {
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_FEET}>Feet (ft)</MenuItem> <MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_FEET}>Feet (ft)</MenuItem>
</TextField> </TextField>
</Grid2> </Grid2>
{IsAdaptiveAgriculture() && ( {getFeatures().grainUnit && (
<Grid2 size={{ xs: 12 }}> <Grid2 size={{ xs: 12 }}>
<TextField <TextField
select select

View file

@ -7,12 +7,7 @@ import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon"; import CNHiIcon from "products/CommonIcons/cnhiIcon";
import LibraCartIcon from "products/CommonIcons/libracartIcon"; import LibraCartIcon from "products/CommonIcons/libracartIcon";
//import AgLogo from "assets/whitelabels/AdaptiveAgriculture/AGLogoSquare.png"; //import AgLogo from "assets/whitelabels/AdaptiveAgriculture/AGLogoSquare.png";
import { import { getFeatures } from "services/whiteLabel";
IsAdaptiveAgriculture
// IsAdCon,
// IsMiVent,
// IsOmniAir
} from "services/whiteLabel";
import FeatureCard from "./FeatureCard"; import FeatureCard from "./FeatureCard";
//import { ImgIcon } from "common/ImgIcon"; //import { ImgIcon } from "common/ImgIcon";
//images to import for the image in the card for the feature make sure to have a 2:1 aspect ratio to keep the cards symmetrical //images to import for the image in the card for the feature make sure to have a 2:1 aspect ratio to keep the cards symmetrical
@ -90,19 +85,9 @@ export default function Marketplace() {
useEffect(() => { useEffect(() => {
let list: ProductDetails[] = []; let list: ProductDetails[] = [];
if (IsAdaptiveAgriculture() || user.hasFeature("admin")) { if (getFeatures().marketplace || user.hasFeature("admin")) {
list = list.concat(agFeatureList); list = list.concat(agFeatureList);
} }
// if(IsAdCon()){
// list = list.concat(constructionFeatureList)
// }
// if(IsOmniAir()){
// list = list.concat(aviationFeatureList)
// }
// if(IsMiVent()){
// list = list.concat(miningFeatureList)
// }
// list = list.concat(universalFeatureList)
setFeaturList(list); setFeaturList(list);
}, []); }, []);

24
src/utils/auth0Config.ts Normal file
View file

@ -0,0 +1,24 @@
import { getWhitelabel } from 'services/whiteLabel'
/** True when Auth0 env + whitelabel client ID are present; avoids mounting Auth0Provider on offline / local LAN builds. */
export function isAuth0Configured(): boolean {
const wl = getWhitelabel()
const domain = String(import.meta.env.VITE_AUTH0_CLIENT_DOMAIN ?? '').trim()
const clientRaw = wl.auth0ClientId ?? import.meta.env.VITE_AUTH0_CLIENT_ID
const clientId = String(clientRaw ?? '').trim()
return domain.length > 0 && clientId.length > 0
}
/**
* auth0-spa-js only runs in a "secure context" (HTTPS, http://localhost, http://127.0.0.1, etc.).
* Plain http://192.168.x.x fails — same check as `window.isSecureContext`.
*/
export function isAuth0SpaOriginAllowed(): boolean {
if (typeof window === 'undefined') return true
return window.isSecureContext
}
/** Mount Auth0 only when credentials exist and the browser will let the SDK run. */
export function shouldMountAuth0Provider(): boolean {
return isAuth0Configured() && isAuth0SpaOriginAllowed()
}

View file

@ -1,12 +1,58 @@
import { defineConfig } from 'vite' import { defineConfig, type Plugin, type UserConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths' import tsconfigPaths from 'vite-tsconfig-paths'
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import * as path from 'path' // ✅ Import path module import * as path from 'path' // ✅ Import path module
import { readFileSync, renameSync, existsSync, unlinkSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
const rootDir = path.dirname(fileURLToPath(import.meta.url))
const LOCALNET_MODE = 'localnet'
/** Dev server: serve the minimal LAN shell (indexLocal.html) instead of the full index with third-party bootstraps. */
function useLocalnetShellHtml (mode: string, command: string): Plugin {
if (mode !== LOCALNET_MODE || command !== 'serve') {
return { name: 'localnet-shell-html-noop' }
}
return {
name: 'localnet-shell-html',
apply: 'serve',
transformIndexHtml: {
order: 'pre',
handler () {
return readFileSync(path.join(rootDir, 'indexLocal.html'), 'utf-8')
},
},
}
}
/** After build, LAN shell is emitted as indexLocal.html; rename to index.html so nginx and PWA still use /. */
function emitLocalnetShellAsIndexHtml (mode: string): Plugin {
if (mode !== LOCALNET_MODE) {
return { name: 'emit-localnet-shell-as-index-noop' }
}
return {
name: 'emit-localnet-shell-as-index-html',
closeBundle () {
const outDir = path.join(rootDir, 'build')
const from = path.join(outDir, 'indexLocal.html')
const to = path.join(outDir, 'index.html')
if (existsSync(from)) {
if (existsSync(to)) unlinkSync(to)
renameSync(from, to)
}
},
}
}
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig(({ command, mode }): UserConfig => {
const useLocalnetShell = mode === LOCALNET_MODE
return {
plugins: [ plugins: [
useLocalnetShellHtml(mode, command),
emitLocalnetShellAsIndexHtml(mode),
react(), react(),
tsconfigPaths(), tsconfigPaths(),
VitePWA({ VitePWA({
@ -53,12 +99,15 @@ export default defineConfig({
target: 'esnext', target: 'esnext',
rollupOptions: { rollupOptions: {
input: { input: {
main: path.resolve(__dirname, 'index.html') main: path.join(
} rootDir,
useLocalnetShell ? 'indexLocal.html' : 'index.html'
),
},
} }
}, },
esbuild: { esbuild: {
keepNames: true, // Prevent function name mangling keepNames: true, // Prevent function name mangling
}, },
}
}) })