diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b081da7c7e..33c7c6a796 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -202,7 +202,7 @@ jobs: - name: Install frontend dependencies run: cd frontend && npm ci - name: Type-check frontend - run: cd frontend && npm run prebuild && npm run typecheck:all + run: cd frontend && npm run prep && npm run typecheck:all - name: Lint frontend run: cd frontend && npm run lint - name: Build frontend diff --git a/.gitignore b/.gitignore index 0c20958f2b..f108270428 100644 --- a/.gitignore +++ b/.gitignore @@ -158,6 +158,7 @@ __pycache__/ # Virtual environments .env* +!.env*.example .venv* env*/ venv*/ diff --git a/AGENTS.md b/AGENTS.md index eb4bf7e827..99e563c963 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,16 @@ Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security featur - **Web Server**: `npm run build` then serve dist/ folder - **Development**: `npm run tauri-dev` for desktop dev mode +#### Environment Variables +- All `VITE_*` variables must be declared in the appropriate example file: + - `frontend/config/.env.example` — core, proprietary, and shared vars + - `frontend/config/.env.saas.example` — SaaS-only vars + - `frontend/config/.env.desktop.example` — desktop (Tauri)-only vars +- Never use `|| 'hardcoded-fallback'` inline — put defaults in the example files +- `npm run prep` / `prep:saas` / `prep:desktop` auto-create the env files from examples on first run, and error if any required keys are missing +- These prep scripts run automatically at the start of all `dev*`, `build*`, and `tauri*` commands +- See `frontend/README.md#environment-variables` for full documentation + #### Import Paths - CRITICAL **ALWAYS use `@app/*` for imports.** Do not use `@core/*` or `@proprietary/*` unless explicitly wrapping/extending a lower layer implementation. diff --git a/frontend/.gitignore b/frontend/.gitignore index 1191bbebfc..3755d7fa70 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -14,10 +14,14 @@ # misc .DS_Store +.env +.env.saas +.env.desktop .env.local .env.development.local .env.test.local .env.production.local +!.env*.example npm-debug.log* yarn-debug.log* diff --git a/frontend/README.md b/frontend/README.md index 68746b8f10..e3bd1887b0 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,6 +1,11 @@ -# Getting Started with Create React App +# Frontend +## Environment Variables -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +The frontend requires environment variables to be set before running. `npm run dev` will create a `.env` file for you automatically on first run using the defaults from `config/.env.example` - for most development work this is all you need. + +If you need to configure specific services (Google Drive, Supabase, Stripe, PostHog), edit your local `.env` file. The values in `config/.env.example` show what each variable does and provides sensible defaults where applicable. + +For desktop (Tauri) development, `npm run tauri-dev` will additionally create a `.env.desktop` file from `config/.env.desktop.example`. ## Docker Setup @@ -120,6 +125,11 @@ npm run tauri-dev This will run the gradle runboot command and the tauri dev command concurrently, starting the app once both are stable. +> [!NOTE] +> +> Desktop builds require additional environment variables. See [Environment Variables](#environment-variables) +> above - `npm run tauri-dev` will set these up automatically from `config/.env.desktop.example` on first run. + ### Build To build a deployment of the Tauri app. Use this command in the `frontend` folder: @@ -128,3 +138,8 @@ npm run tauri-build ``` This will bundle the backend and frontend into one executable for each target. Targets can be set within the `tauri.conf.json` file. + +> [!NOTE] +> +> Desktop builds require additional environment variables. See [Environment Variables](#environment-variables) +> above - `npm run tauri-build` will set these up automatically from `config/.env.desktop.example` on first run. diff --git a/frontend/config/.env.desktop.example b/frontend/config/.env.desktop.example new file mode 100644 index 0000000000..65dcea5a05 --- /dev/null +++ b/frontend/config/.env.desktop.example @@ -0,0 +1,11 @@ +# Frontend environment variables for desktop (Tauri) builds. +# Layered on top of .env when running in desktop mode. + +# Desktop backend endpoint — leave blank to use VITE_API_BASE_URL from .env +VITE_DESKTOP_BACKEND_URL= + +# Desktop auth integration +VITE_SAAS_SERVER_URL=https://auth.stirling.com +VITE_SAAS_BACKEND_API_URL=https://api2.stirling.com +VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb +VITE_SUPABASE_URL=https://rficokptxxxxtyzcvgmx.supabase.co diff --git a/frontend/config/.env.example b/frontend/config/.env.example new file mode 100644 index 0000000000..1df035f4d5 --- /dev/null +++ b/frontend/config/.env.example @@ -0,0 +1,20 @@ +# Frontend environment variables for core and proprietary builds. + +# API base URL — use / for same-origin (default for web builds) +VITE_API_BASE_URL=/ + +# Google Drive integration +VITE_GOOGLE_DRIVE_CLIENT_ID= +VITE_GOOGLE_DRIVE_API_KEY= +VITE_GOOGLE_DRIVE_APP_ID= + +# Supabase configuration +VITE_SUPABASE_URL=https://rficokptxxxxtyzcvgmx.supabase.co +VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb + +# Stripe checkout +VITE_STRIPE_PUBLISHABLE_KEY=pk_live_51Q56W2P9mY5IAnSnp3kcxG50uyFMLuhM4fFs774DAP3t88KmlwUrUo31CecpnAZ9FHsNp8xJyOnYNYNVVP6z4oi500q5sFYPEp + +# PostHog analytics +VITE_PUBLIC_POSTHOG_KEY=phc_VOdeYnlevc2T63m3myFGjeBlRcIusRgmhfx6XL5a1iz +VITE_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com diff --git a/frontend/config/.env.saas.example b/frontend/config/.env.saas.example new file mode 100644 index 0000000000..cc14a11dc7 --- /dev/null +++ b/frontend/config/.env.saas.example @@ -0,0 +1,11 @@ +# Frontend environment variables for SaaS builds. +# Layered on top of .env when running in SaaS mode. + +# Userback feedback widget — leave blank to disable +VITE_USERBACK_TOKEN= + +# URL subpath prefix for SaaS deployments (e.g. "app" if serving at /app/) — leave blank for root +VITE_RUN_SUBPATH= + +# Development-only auth bypass — allows unauthenticated access on localhost in dev mode +VITE_DEV_BYPASS_AUTH=false diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d82024705e..b4da534281 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -104,6 +104,7 @@ "@typescript-eslint/parser": "^8.44.1", "@vitejs/plugin-react-swc": "^4.1.0", "@vitest/coverage-v8": "^3.2.4", + "dotenv": "^16.4.7", "dpdm": "^3.14.0", "eslint": "^10.0.2", "jsdom": "^27.0.0", @@ -114,6 +115,7 @@ "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "puppeteer": "^24.25.0", + "tsx": "^4.19.4", "typescript": "^5.9.2", "typescript-eslint": "^8.44.1", "vite": "^7.1.7", @@ -7518,6 +7520,19 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dpdm": { "version": "3.15.1", "resolved": "https://registry.npmjs.org/dpdm/-/dpdm-3.15.1.tgz", @@ -8540,6 +8555,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", @@ -12145,6 +12173,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/responselike": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", @@ -13384,6 +13422,41 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7074f80970..b78dafa0b7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -79,36 +79,39 @@ "web-vitals": "^5.1.0" }, "scripts": { - "pretauri-build": "node scripts/build-provisioner.mjs", - "predev": "npm run generate-icons", - "dev": "vite", - "dev:core": "vite --mode core", - "dev:proprietary": "vite --mode proprietary", - "dev:saas": "vite --mode saas", - "dev:desktop": "vite --mode desktop", - "prebuild": "npm run generate-icons", + "prep": "tsx scripts/setup-env.ts && npm run generate-icons", + "prep:saas": "tsx scripts/setup-env.ts --saas && npm run generate-icons", + "prep:desktop": "tsx scripts/setup-env.ts --desktop && npm run generate-icons", + "prep:desktop-build": "node scripts/build-provisioner.mjs && npm run prep:desktop", + "dev": "npm run prep && vite", + "dev:core": "npm run prep && vite --mode core", + "dev:proprietary": "npm run prep && vite --mode proprietary", + "dev:saas": "npm run prep:saas && vite --mode saas", + "dev:desktop": "npm run prep:desktop && vite --mode desktop", "lint": "npm run lint:eslint && npm run lint:cycles", "lint:eslint": "eslint --max-warnings=0", "lint:cycles": "dpdm src --circular --no-warning --no-tree --exit-code circular:1", - "build": "vite build", - "build:core": "vite build --mode core", - "build:proprietary": "vite build --mode proprietary", - "build:saas": "vite build --mode saas", - "build:desktop": "vite build --mode desktop", + "build": "npm run prep && vite build", + "build:core": "npm run prep && vite build --mode core", + "build:proprietary": "npm run prep && vite build --mode proprietary", + "build:saas": "npm run prep:saas && vite build --mode saas", + "build:desktop": "npm run prep:desktop && vite build --mode desktop", "preview": "vite preview", - "tauri-dev": "tauri dev --no-watch", - "tauri-build": "tauri build", - "tauri-build-dev": "tauri build --no-bundle", - "tauri-build-dev-mac": "tauri build --bundles app", - "tauri-build-dev-windows": "tauri build --bundles nsis", - "tauri-build-dev-linux": "tauri build --bundles appimage", + "tauri-dev": "npm run prep:desktop && tauri dev --no-watch", + "tauri-build": "npm run prep:desktop-build && tauri build", + "_tauri-build-dev": "npm run prep:desktop && tauri build", + "tauri-build-dev": "npm run _tauri-build-dev -- --no-bundle", + "tauri-build-dev-mac": "npm run _tauri-build-dev -- --bundles app", + "tauri-build-dev-windows": "npm run _tauri-build-dev -- --bundles nsis", + "tauri-build-dev-linux": "npm run _tauri-build-dev -- --bundles appimage", "tauri-clean": "cd src-tauri && cargo clean && cd .. && rm -rf dist build", "typecheck": "npm run typecheck:proprietary", "typecheck:core": "tsc --noEmit --project src/core/tsconfig.json", "typecheck:proprietary": "tsc --noEmit --project src/proprietary/tsconfig.json", "typecheck:saas": "tsc --noEmit --project src/saas/tsconfig.json", "typecheck:desktop": "tsc --noEmit --project src/desktop/tsconfig.json", - "typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop", + "typecheck:scripts": "tsc --noEmit --project scripts/tsconfig.json", + "typecheck:all": "npm run typecheck:core && npm run typecheck:proprietary && npm run typecheck:saas && npm run typecheck:desktop && npm run typecheck:scripts", "check": "npm run typecheck && npm run lint && npm run test:run", "generate-licenses": "node scripts/generate-licenses.js", "generate-icons": "node scripts/generate-icons.js", @@ -160,6 +163,7 @@ "@typescript-eslint/parser": "^8.44.1", "@vitejs/plugin-react-swc": "^4.1.0", "@vitest/coverage-v8": "^3.2.4", + "dotenv": "^16.4.7", "dpdm": "^3.14.0", "eslint": "^10.0.2", "jsdom": "^27.0.0", @@ -171,6 +175,7 @@ "postcss-simple-vars": "^7.0.1", "puppeteer": "^24.25.0", "typescript": "^5.9.2", + "tsx": "^4.19.4", "typescript-eslint": "^8.44.1", "vite": "^7.1.7", "vite-plugin-static-copy": "^3.1.4", diff --git a/frontend/scripts/setup-env.ts b/frontend/scripts/setup-env.ts new file mode 100644 index 0000000000..00ec03df00 --- /dev/null +++ b/frontend/scripts/setup-env.ts @@ -0,0 +1,88 @@ +/** + * Copies missing env files from their .example templates, and warns about + * any keys present in the example but not set in the environment. + * Also warns about any VITE_ vars set in the environment that aren't listed + * in any example file. + * + * Usage: + * tsx scripts/setup-env.ts # checks .env + * tsx scripts/setup-env.ts --desktop # also checks .env.desktop + * tsx scripts/setup-env.ts --saas # also checks .env.saas + */ + +import { existsSync, copyFileSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { config, parse } from 'dotenv'; + +// npm scripts run from the directory containing package.json (frontend/) +const root = process.cwd(); +const args = process.argv.slice(2); +const isDesktop = args.includes('--desktop'); +const isSaas = args.includes('--saas'); + +console.log('setup-env: see frontend/README.md#environment-variables for documentation'); + +function getExampleKeys(exampleFile: string): string[] { + const examplePath = join(root, exampleFile); + if (!existsSync(examplePath)) return []; + return Object.keys(parse(readFileSync(examplePath, 'utf-8'))); +} + +function ensureEnvFile(envFile: string, exampleFile: string): boolean { + const envPath = join(root, envFile); + const examplePath = join(root, exampleFile); + + if (!existsSync(examplePath)) { + console.warn(`setup-env: ${exampleFile} not found, skipping ${envFile}`); + return false; + } + + if (!existsSync(envPath)) { + copyFileSync(examplePath, envPath); + console.log(`setup-env: created ${envFile} from ${exampleFile}`); + } + + config({ path: envPath }); + + const missing = getExampleKeys(exampleFile).filter(k => !(k in process.env)); + + if (missing.length > 0) { + console.error( + `setup-env: ${envFile} is missing keys from ${exampleFile}:\n` + + missing.map(k => ` ${k}`).join('\n') + + '\n Add them manually or delete your local file to re-copy from the example.' + ); + return true; + } + + return false; +} + +let failed = false; +failed = ensureEnvFile('.env', 'config/.env.example') || failed; + +if (isDesktop) { + failed = ensureEnvFile('.env.desktop', 'config/.env.desktop.example') || failed; +} + +if (isSaas) { + failed = ensureEnvFile('.env.saas', 'config/.env.saas.example') || failed; +} + +// Warn about any VITE_ vars set in the environment that aren't listed in any example file. +const allExampleKeys = new Set([ + ...getExampleKeys('config/.env.example'), + ...getExampleKeys('config/.env.desktop.example'), + ...getExampleKeys('config/.env.saas.example'), +]); +const unknownViteVars = Object.keys(process.env) + .filter(k => k.startsWith('VITE_') && !allExampleKeys.has(k)); +if (unknownViteVars.length > 0) { + console.warn( + 'setup-env: the following VITE_ vars are set but not listed in any example file:\n' + + unknownViteVars.map(k => ` ${k}`).join('\n') + + '\n Add them to the appropriate config/.env.*.example file if they are required.' + ); +} + +if (failed) process.exit(1); diff --git a/frontend/scripts/tsconfig.json b/frontend/scripts/tsconfig.json new file mode 100644 index 0000000000..99e53f6736 --- /dev/null +++ b/frontend/scripts/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16", + "noEmit": true + }, + "include": ["./**/*.ts"], + "exclude": [] +} diff --git a/frontend/src/core/env.test.ts b/frontend/src/core/env.test.ts new file mode 100644 index 0000000000..794f0551b4 --- /dev/null +++ b/frontend/src/core/env.test.ts @@ -0,0 +1,58 @@ +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join, extname } from 'path'; +import { fileURLToPath } from 'url'; +import { describe, it, expect } from 'vitest'; + +// frontend/ root — this file lives at src/core/env.test.ts +const frontendRoot = join(fileURLToPath(import.meta.url), '../../..'); + +function parseEnvKeys(content: string): Set { + const keys = new Set(); + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=/); + if (match) keys.add(match[1]); + } + } + return keys; +} + +function collectSourceFiles(dir: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(dir)) { + const fullPath = join(dir, entry); + const stat = statSync(fullPath); + if (stat.isDirectory() && entry !== 'node_modules' && entry !== 'assets') { + files.push(...collectSourceFiles(fullPath)); + } else if (stat.isFile() && (extname(entry) === '.ts' || extname(entry) === '.tsx') && !entry.endsWith('.d.ts')) { + files.push(fullPath); + } + } + return files; +} + +function findViteEnvVars(srcDir: string): Set { + const vars = new Set(); + for (const file of collectSourceFiles(srcDir)) { + const content = readFileSync(file, 'utf-8'); + for (const match of content.matchAll(/import\.meta\.env\.(VITE_\w+)/g)) { + vars.add(match[1]); + } + } + return vars; +} + +describe('env vars', () => { + it('every VITE_ var used in source is present in an example env file', () => { + const baseEnv = readFileSync(join(frontendRoot, 'config/.env.example'), 'utf-8'); + const desktopEnv = readFileSync(join(frontendRoot, 'config/.env.desktop.example'), 'utf-8'); + const saasEnv = readFileSync(join(frontendRoot, 'config/.env.saas.example'), 'utf-8'); + + const exampleKeys = new Set([...parseEnvKeys(baseEnv), ...parseEnvKeys(desktopEnv), ...parseEnvKeys(saasEnv)]); + const sourceVars = findViteEnvVars(join(frontendRoot, 'src')); + + const missing = [...sourceVars].filter(v => !exampleKeys.has(v)); + expect(missing, `Missing from 'frontend/config/.env.example' files: ${missing.join(', ')}`).toHaveLength(0); + }); +}); diff --git a/frontend/src/core/services/apiClientConfig.ts b/frontend/src/core/services/apiClientConfig.ts index 9038da5eb1..716c73ace7 100644 --- a/frontend/src/core/services/apiClientConfig.ts +++ b/frontend/src/core/services/apiClientConfig.ts @@ -15,5 +15,5 @@ export function getApiBaseUrl(): string { return (window as any).STIRLING_PDF_API_BASE_URL; } - return import.meta.env.VITE_API_BASE_URL || '/'; + return import.meta.env.VITE_API_BASE_URL; } diff --git a/frontend/src/core/services/supabaseClient.ts b/frontend/src/core/services/supabaseClient.ts index 77bebe1b81..c0ace3002e 100644 --- a/frontend/src/core/services/supabaseClient.ts +++ b/frontend/src/core/services/supabaseClient.ts @@ -1,7 +1,7 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js'; -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://rficokptxxxxtyzcvgmx.supabase.co'; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb'; +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY; // Check if Supabase is configured export const isSupabaseConfigured = !!(supabaseUrl && supabaseAnonKey); diff --git a/frontend/src/desktop/constants/connection.ts b/frontend/src/desktop/constants/connection.ts index cc674048d1..9467fcb1c8 100644 --- a/frontend/src/desktop/constants/connection.ts +++ b/frontend/src/desktop/constants/connection.ts @@ -2,22 +2,14 @@ * Connection-related constants for desktop app */ -// SaaS server URL from environment variable -// The SaaS authentication server (Supabase) -export const STIRLING_SAAS_URL: string = import.meta.env.VITE_SAAS_SERVER_URL || ''; +// SaaS authentication server URL +export const STIRLING_SAAS_URL: string = import.meta.env.VITE_SAAS_SERVER_URL; -// SaaS backend API URL from environment variable -// The Stirling SaaS backend API server (for team endpoints, etc.) -export const STIRLING_SAAS_BACKEND_API_URL: string = import.meta.env.VITE_SAAS_BACKEND_API_URL || ''; +// Stirling SaaS backend API server (for team endpoints, etc.) +export const STIRLING_SAAS_BACKEND_API_URL: string = import.meta.env.VITE_SAAS_BACKEND_API_URL; -// Supabase publishable key from environment variable -// Used for SaaS authentication -export const SUPABASE_KEY: string = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb'; +// Supabase publishable key — used for SaaS authentication +export const SUPABASE_KEY: string = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY; // Desktop deep link callback for Supabase email confirmations export const DESKTOP_DEEP_LINK_CALLBACK = 'stirlingpdf://auth/callback'; - -// Validation warnings -if (!STIRLING_SAAS_BACKEND_API_URL) { - console.warn('[Desktop Connection] VITE_SAAS_BACKEND_API_URL not configured - SaaS backend APIs (teams, etc.) will not work'); -} diff --git a/frontend/src/desktop/hooks/useEndpointConfig.ts b/frontend/src/desktop/hooks/useEndpointConfig.ts index a4f093f266..87216cea74 100644 --- a/frontend/src/desktop/hooks/useEndpointConfig.ts +++ b/frontend/src/desktop/hooks/useEndpointConfig.ts @@ -401,8 +401,7 @@ export function useMultipleEndpointsEnabled(endpoints: string[]): { // Default backend URL from environment variables const DEFAULT_BACKEND_URL = import.meta.env.VITE_DESKTOP_BACKEND_URL - || import.meta.env.VITE_API_BASE_URL - || ''; + || import.meta.env.VITE_API_BASE_URL; /** * Desktop override exposing the backend URL based on connection mode. diff --git a/frontend/src/desktop/services/apiClientConfig.ts b/frontend/src/desktop/services/apiClientConfig.ts index 2245c6e681..a936392c81 100644 --- a/frontend/src/desktop/services/apiClientConfig.ts +++ b/frontend/src/desktop/services/apiClientConfig.ts @@ -19,7 +19,7 @@ export function getApiBaseUrl(): string { return (window as any).STIRLING_PDF_API_BASE_URL; } - return import.meta.env.VITE_API_BASE_URL || '/'; + return import.meta.env.VITE_API_BASE_URL; } // In Tauri mode, return empty string as placeholder diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index c610427863..96383ab779 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -12,8 +12,8 @@ import posthog from 'posthog-js'; import { PostHogProvider } from '@posthog/react'; import { BASE_PATH } from '@app/constants/app'; -posthog.init('phc_VOdeYnlevc2T63m3myFGjeBlRcIusRgmhfx6XL5a1iz', { - api_host: 'https://eu.i.posthog.com', +posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, { + api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST, defaults: '2025-05-24', capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this debug: false, diff --git a/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx b/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx index 8d4f5d0fca..2743a49061 100644 --- a/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx +++ b/frontend/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx @@ -19,7 +19,7 @@ import { SuccessStage } from '@app/components/shared/stripeCheckout/stages/Succe import { ErrorStage } from '@app/components/shared/stripeCheckout/stages/ErrorStage'; // Validate Stripe key (static validation, no dynamic imports) -const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_live_51Q56W2P9mY5IAnSnp3kcxG50uyFMLuhM4fFs774DAP3t88KmlwUrUo31CecpnAZ9FHsNp8xJyOnYNYNVVP6z4oi500q5sFYPEp'; +const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY; if (!STRIPE_KEY) { console.error( diff --git a/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx b/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx index 6686a1496f..84664f873f 100644 --- a/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx +++ b/frontend/src/proprietary/components/shared/stripeCheckout/stages/PaymentStage.tsx @@ -6,7 +6,7 @@ import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe import { PlanTier } from '@app/services/licenseService'; // Load Stripe once -const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_live_51Q56W2P9mY5IAnSnp3kcxG50uyFMLuhM4fFs774DAP3t88KmlwUrUo31CecpnAZ9FHsNp8xJyOnYNYNVVP6z4oi500q5sFYPEp'; +const STRIPE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY; const stripePromise = STRIPE_KEY ? loadStripe(STRIPE_KEY) : null; interface PaymentStageProps { diff --git a/frontend/src/proprietary/utils/protocolDetection.ts b/frontend/src/proprietary/utils/protocolDetection.ts index 5cfbe805a4..4f7e328c55 100644 --- a/frontend/src/proprietary/utils/protocolDetection.ts +++ b/frontend/src/proprietary/utils/protocolDetection.ts @@ -9,7 +9,7 @@ * @returns true if key exists and has valid format */ export function isStripeConfigured(): boolean { - const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_live_51Q56W2P9mY5IAnSnp3kcxG50uyFMLuhM4fFs774DAP3t88KmlwUrUo31CecpnAZ9FHsNp8xJyOnYNYNVVP6z4oi500q5sFYPEp'; + const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY; return !!stripeKey && stripeKey.startsWith('pk_'); } diff --git a/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx b/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx index 3f7e13a756..61c164b236 100644 --- a/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx +++ b/frontend/src/saas/components/shared/StripeCheckoutSaas.tsx @@ -6,7 +6,7 @@ import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe import { supabase } from '@app/auth/supabase'; import { Z_INDEX_OVER_SETTINGS_MODAL } from '@app/styles/zIndex'; -const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_DEFAULT_KEY); +const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); export type PurchaseType = 'subscription' | 'credits'; export type CreditsPack = 'xsmall' | 'small' | 'medium' | 'large' | null; diff --git a/frontend/src/saas/services/apiClient.ts b/frontend/src/saas/services/apiClient.ts index 2508cce493..242823e6c4 100644 --- a/frontend/src/saas/services/apiClient.ts +++ b/frontend/src/saas/services/apiClient.ts @@ -31,7 +31,7 @@ function decodeJwtPayload(token: string): Record | null { // Create axios instance with default config const apiClient = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL || '/', // Use env var or relative path (proxied by Vite in dev) + baseURL: import.meta.env.VITE_API_BASE_URL, responseType: 'json', }); diff --git a/frontend/src/saas/utils/pathUtils.ts b/frontend/src/saas/utils/pathUtils.ts index 46daa5cadb..59dca0bd84 100644 --- a/frontend/src/saas/utils/pathUtils.ts +++ b/frontend/src/saas/utils/pathUtils.ts @@ -1,6 +1,6 @@ import { URL_TO_TOOL_MAP } from '@app/utils/urlMapping' -const SUBPATH = (import.meta.env.VITE_RUN_SUBPATH || '').replace(/^\/|\/$/g, '') // "app" or "" +const SUBPATH = import.meta.env.VITE_RUN_SUBPATH.replace(/^\/|\/$/g, '') // "app" or "" /** * Normalize pathname by stripping subpath prefix and trailing slashes diff --git a/frontend/vite-env.d.ts b/frontend/vite-env.d.ts index f483e59d43..54c9405987 100644 --- a/frontend/vite-env.d.ts +++ b/frontend/vite-env.d.ts @@ -1,10 +1,26 @@ /// interface ImportMetaEnv { - readonly VITE_PUBLIC_POSTHOG_KEY: string; - readonly VITE_PUBLIC_POSTHOG_HOST: string; + // Used by all builds (.env) + readonly VITE_API_BASE_URL: string; + readonly VITE_GOOGLE_DRIVE_CLIENT_ID: string; + readonly VITE_GOOGLE_DRIVE_API_KEY: string; + readonly VITE_GOOGLE_DRIVE_APP_ID: string; readonly VITE_SUPABASE_URL: string; readonly VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY: string; + readonly VITE_STRIPE_PUBLISHABLE_KEY: string; + readonly VITE_PUBLIC_POSTHOG_KEY: string; + readonly VITE_PUBLIC_POSTHOG_HOST: string; + + // SaaS only (.env.saas) + readonly VITE_USERBACK_TOKEN: string; + readonly VITE_RUN_SUBPATH: string; + readonly VITE_DEV_BYPASS_AUTH: string; + + // Desktop only (.env.desktop) + readonly VITE_DESKTOP_BACKEND_URL: string; + readonly VITE_SAAS_SERVER_URL: string; + readonly VITE_SAAS_BACKEND_API_URL: string; } interface ImportMeta { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5670928060..4e36220aaf 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -27,23 +27,6 @@ export default defineConfig(({ mode }) => { ? (mode as BuildMode) : process.env.DISABLE_ADDITIONAL_FEATURES === 'true' ? 'core' : 'proprietary'; - // Validate required environment variables for desktop builds - if (effectiveMode === 'desktop') { - const requiredEnvVars = [ - 'VITE_SAAS_SERVER_URL', - 'VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY', - 'VITE_SAAS_BACKEND_API_URL', - ]; - - const missingVars = requiredEnvVars.filter(varName => !env[varName]); - - if (missingVars.length > 0) { - throw new Error( - `Desktop build failed: Missing required environment variables:\n${missingVars.map(v => ` - ${v}`).join('\n')}\n\nPlease set these variables before building the desktop app.` - ); - } - } - const tsconfigProject = TSCONFIG_MAP[effectiveMode]; return {