diff --git a/src/App.tsx b/src/App.tsx index 9c26ad5..f5f5f56 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,6 +23,7 @@ import useAccessibility from './hooks/useAccessibility'; import PageLayout from './components/PageLayout'; import { ThemeProvider } from './contexts/ThemeContext'; +import { config } from './config/env'; const LandingContent: React.FC = () => ( <> @@ -42,13 +43,7 @@ function App() { useAccessibility(); // Support local testing via environment variable - const enableLocalAuth = - process.env.REACT_APP_ENABLE_LOCAL_AUTH === 'true' || - process.env.REACT_APP_ENABLE_LOCAL_AUTH === '1'; - - const enableLocalDocs = - process.env.REACT_APP_ENABLE_LOCAL_DOCS === 'true' || - process.env.REACT_APP_ENABLE_LOCAL_DOCS === '1'; + const { enableLocalAuth, enableLocalDocs } = config; const isDocsHost = (typeof window !== 'undefined' && diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 1f6f173..3a70842 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { config } from '../config/env'; import { motion, AnimatePresence } from 'framer-motion'; import { BookOpen, Plus } from 'lucide-react'; import { useLocation } from 'react-router-dom'; @@ -23,8 +24,7 @@ const Dashboard: React.FC = () => { const refreshUserData = async () => { try { const token = localStorage.getItem('accessToken'); - const apiBaseUrl = - process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001'; + const apiBaseUrl = config.apiBaseUrl; const response = await fetch(`${apiBaseUrl}/api/auth/me`, { headers: { Authorization: `Bearer ${token}`, diff --git a/src/components/DeviceConnect.tsx b/src/components/DeviceConnect.tsx index 6557db3..5a3fcdf 100644 --- a/src/components/DeviceConnect.tsx +++ b/src/components/DeviceConnect.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { config } from '../config/env'; import { useSearchParams, useNavigate, @@ -84,8 +85,7 @@ const DeviceConnect: React.FC = () => { setErrorMessage(''); try { - const apiBaseUrl = - process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001'; + const apiBaseUrl = config.apiBaseUrl; const token = localStorage.getItem('accessToken'); const response = await fetch(`${apiBaseUrl}/api/oauth/device/confirm`, { diff --git a/src/components/EarlyAccessForm.tsx b/src/components/EarlyAccessForm.tsx index d2f85de..0ebb1d1 100644 --- a/src/components/EarlyAccessForm.tsx +++ b/src/components/EarlyAccessForm.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { config } from '../config/env'; import { motion } from 'framer-motion'; import { CheckCircle, Send, AlertCircle } from 'lucide-react'; import emailjs from '@emailjs/browser'; @@ -33,20 +34,15 @@ const EarlyAccessForm: React.FC = () => { try { // EmailJS configuration - const serviceId = - process.env.REACT_APP_EMAILJS_SERVICE_ID || 'your_service_id'; - const welcomeTemplateId = - process.env.REACT_APP_EMAILJS_WELCOME_TEMPLATE_ID || - 'your_welcome_template_id'; - const notificationTemplateId = - process.env.REACT_APP_EMAILJS_NOTIFICATION_TEMPLATE_ID || - 'your_notification_template_id'; - const publicKey = - process.env.REACT_APP_EMAILJS_PUBLIC_KEY || 'your_public_key'; - const fromEmail = - process.env.REACT_APP_FROM_EMAIL || 'hello@refactron.dev'; - const notificationEmail = - process.env.REACT_APP_NOTIFICATION_EMAIL || 'hello@refactron.dev'; + const { emailjs: emailjsConfig, emails } = config; + const { + serviceId, + welcomeTemplateId, + notificationTemplateId, + publicKey, + } = emailjsConfig; + const fromEmail = emails.from; + const notificationEmail = emails.notification; // Check if environment variables are properly set if ( diff --git a/src/components/EarlyAccessModal.tsx b/src/components/EarlyAccessModal.tsx index 5ef5431..506894c 100644 --- a/src/components/EarlyAccessModal.tsx +++ b/src/components/EarlyAccessModal.tsx @@ -1,5 +1,6 @@ 'use client'; import React, { useState } from 'react'; +import { config } from '../config/env'; import { motion, AnimatePresence } from 'framer-motion'; import { X, CheckCircle, AlertCircle } from 'lucide-react'; import emailjs from '@emailjs/browser'; @@ -44,20 +45,15 @@ const EarlyAccessModal: React.FC = ({ setIsLoading(true); try { - const serviceId = - process.env.REACT_APP_EMAILJS_SERVICE_ID || 'your_service_id'; - const welcomeTemplateId = - process.env.REACT_APP_EMAILJS_WELCOME_TEMPLATE_ID || - 'your_welcome_template_id'; - const notificationTemplateId = - process.env.REACT_APP_EMAILJS_NOTIFICATION_TEMPLATE_ID || - 'your_notification_template_id'; - const publicKey = - process.env.REACT_APP_EMAILJS_PUBLIC_KEY || 'your_public_key'; - const fromEmail = - process.env.REACT_APP_FROM_EMAIL || 'hello@refactron.dev'; - const notificationEmail = - process.env.REACT_APP_NOTIFICATION_EMAIL || 'hello@refactron.dev'; + const { emailjs: emailjsConfig, emails } = config; + const { + serviceId, + welcomeTemplateId, + notificationTemplateId, + publicKey, + } = emailjsConfig; + const fromEmail = emails.from; + const notificationEmail = emails.notification; if ( serviceId === 'your_service_id' || diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 2a3e9b1..9fa86bd 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,4 +1,5 @@ import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { config } from '../config/env'; import { motion } from 'framer-motion'; import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; import * as Sentry from '@sentry/react'; @@ -32,10 +33,7 @@ class ErrorBoundary extends Component { }); // Log to Sentry in production if configured - if ( - process.env.NODE_ENV === 'production' && - process.env.REACT_APP_SENTRY_DSN - ) { + if (config.isProduction && config.sentryDsn) { Sentry.captureException(error, { extra: { componentStack: errorInfo.componentStack, diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 7e64668..6933608 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; +import { config } from '../config/env'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowRight, Eye, EyeOff } from 'lucide-react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; @@ -195,8 +196,7 @@ const LoginForm: React.FC = () => { setIsLoading(true); try { - const apiBaseUrl = - process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001'; + const apiBaseUrl = config.apiBaseUrl; const response = await fetch(`${apiBaseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/src/components/SignupForm.tsx b/src/components/SignupForm.tsx index 924900e..fbf4cbe 100644 --- a/src/components/SignupForm.tsx +++ b/src/components/SignupForm.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; +import { config } from '../config/env'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowRight, Eye, EyeOff } from 'lucide-react'; import { Link, useNavigate } from 'react-router-dom'; @@ -154,8 +155,7 @@ const SignupForm: React.FC = () => { setIsLoading(true); try { - const apiBaseUrl = - process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001'; + const apiBaseUrl = config.apiBaseUrl; const response = await fetch(`${apiBaseUrl}/api/auth/signup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/src/components/VerifyEmail.tsx b/src/components/VerifyEmail.tsx index 04dc689..eca922a 100644 --- a/src/components/VerifyEmail.tsx +++ b/src/components/VerifyEmail.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { config } from '../config/env'; import { useSearchParams, Link, useLocation } from 'react-router-dom'; import { Check, X, ArrowRight } from 'lucide-react'; import { motion } from 'framer-motion'; @@ -36,8 +37,7 @@ const VerifyEmail: React.FC = () => { setStatus('loading'); const verifyToken = async () => { try { - const apiBaseUrl = - process.env.REACT_APP_API_BASE_URL || 'http://localhost:3001'; + const apiBaseUrl = config.apiBaseUrl; const response = await fetch(`${apiBaseUrl}/api/auth/verify-email`, { method: 'POST', headers: { diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..bf23d4b --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,56 @@ +/** + * Centralized environment configuration for the Refactron frontend. + * All process.env.REACT_APP_* usages should be moved here. + */ + +const getEnv = (name: string, defaultValue = ''): string => { + return process.env[name] || defaultValue; +}; + +const getBoolEnv = (name: string): boolean => { + const val = process.env[name]; + return val === 'true' || val === '1'; +}; + +export const config = { + // Application Environment + isProduction: process.env.NODE_ENV === 'production', + isDevelopment: process.env.NODE_ENV === 'development', + isTest: process.env.NODE_ENV === 'test', + + // API Configuration + apiBaseUrl: getEnv('REACT_APP_API_BASE_URL', 'http://localhost:3001'), + + // Feature Toggles + enableLocalAuth: getBoolEnv('REACT_APP_ENABLE_LOCAL_AUTH'), + enableLocalDocs: getBoolEnv('REACT_APP_ENABLE_LOCAL_DOCS'), + + // OAuth Configuration + googleClientId: getEnv('REACT_APP_GOOGLE_CLIENT_ID'), + githubClientId: getEnv('REACT_APP_GITHUB_CLIENT_ID'), + + // Analytics & Monitoring + sentryDsn: getEnv('REACT_APP_SENTRY_DSN'), + + // EmailJS Configuration + emailjs: { + serviceId: getEnv('REACT_APP_EMAILJS_SERVICE_ID', 'your_service_id'), + welcomeTemplateId: getEnv( + 'REACT_APP_EMAILJS_WELCOME_TEMPLATE_ID', + 'your_welcome_template_id' + ), + notificationTemplateId: getEnv( + 'REACT_APP_EMAILJS_NOTIFICATION_TEMPLATE_ID', + 'your_notification_template_id' + ), + publicKey: getEnv('REACT_APP_EMAILJS_PUBLIC_KEY', 'your_public_key'), + }, + + // Contact Emails + emails: { + from: getEnv('REACT_APP_FROM_EMAIL', 'hello@refactron.dev'), + notification: getEnv('REACT_APP_NOTIFICATION_EMAIL', 'hello@refactron.dev'), + }, +}; + +export default config; diff --git a/src/hooks/useRepositories.tsx b/src/hooks/useRepositories.tsx index fd3fd45..229e098 100644 --- a/src/hooks/useRepositories.tsx +++ b/src/hooks/useRepositories.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { config } from '../config/env'; export interface Repository { id: number; @@ -39,7 +40,7 @@ export function useRepositories(): UseRepositoriesResult { try { const response = await fetch( - `${process.env.REACT_APP_API_BASE_URL}/api/github/repositories`, + `${config.apiBaseUrl}/api/github/repositories`, { credentials: 'include', headers: { diff --git a/src/index.tsx b/src/index.tsx index ba9f334..a19e9af 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,7 @@ import * as Sentry from '@sentry/react'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import { config } from './config/env'; // Apply dark theme immediately if (typeof document !== 'undefined') { @@ -11,10 +12,9 @@ if (typeof document !== 'undefined') { } Sentry.init({ - dsn: process.env.REACT_APP_SENTRY_DSN, + dsn: config.sentryDsn, environment: process.env.NODE_ENV, - enabled: - process.env.NODE_ENV === 'production' && !!process.env.REACT_APP_SENTRY_DSN, + enabled: config.isProduction && !!config.sentryDsn, }); const root = ReactDOM.createRoot( diff --git a/src/utils/oauth.test.ts b/src/utils/oauth.test.ts index c51f58c..8e5f91c 100644 --- a/src/utils/oauth.test.ts +++ b/src/utils/oauth.test.ts @@ -8,6 +8,22 @@ import { handleOAuthCallback, isOAuthProviderConfigured, } from './oauth'; +import { config } from '../config/env'; + +// Mock the config module +jest.mock('../config/env', () => ({ + __esModule: true, + config: { + googleClientId: '', + githubClientId: '', + apiBaseUrl: 'http://localhost:3001', + }, + default: { + googleClientId: '', + githubClientId: '', + apiBaseUrl: 'http://localhost:3001', + }, +})); // Mock window.location delete (window as any).location; @@ -51,9 +67,9 @@ describe('OAuth utility functions', () => { beforeEach(() => { sessionStorage.clear(); window.location.href = ''; - // Clear environment variables - delete process.env.REACT_APP_GOOGLE_CLIENT_ID; - delete process.env.REACT_APP_GITHUB_CLIENT_ID; + config.googleClientId = ''; + config.githubClientId = ''; + config.apiBaseUrl = 'http://localhost:3001'; // Clear fetch mock global.fetch = jest.fn(); }); @@ -64,7 +80,7 @@ describe('OAuth utility functions', () => { describe('isOAuthProviderConfigured', () => { it('should return true when Google client ID is configured', () => { - process.env.REACT_APP_GOOGLE_CLIENT_ID = 'test-google-client-id'; + (config as any).googleClientId = 'test-google-client-id'; expect(isOAuthProviderConfigured('google')).toBe(true); }); @@ -73,7 +89,7 @@ describe('OAuth utility functions', () => { }); it('should return true when GitHub client ID is configured', () => { - process.env.REACT_APP_GITHUB_CLIENT_ID = 'test-github-client-id'; + (config as any).githubClientId = 'test-github-client-id'; expect(isOAuthProviderConfigured('github')).toBe(true); }); @@ -169,7 +185,7 @@ describe('OAuth utility functions', () => { }); it('should redirect to Google OAuth URL with correct parameters for login', async () => { - process.env.REACT_APP_GOOGLE_CLIENT_ID = 'test-google-client-id'; + (config as any).googleClientId = 'test-google-client-id'; await initiateOAuth('google', 'login', { redirectUri: 'http://localhost:3000/auth/callback', @@ -188,7 +204,7 @@ describe('OAuth utility functions', () => { }); it('should redirect to Google OAuth URL with consent prompt for signup', async () => { - process.env.REACT_APP_GOOGLE_CLIENT_ID = 'test-google-client-id'; + (config as any).googleClientId = 'test-google-client-id'; await initiateOAuth('google', 'signup', { redirectUri: 'http://localhost:3000/auth/callback', @@ -198,7 +214,7 @@ describe('OAuth utility functions', () => { }); it('should redirect to GitHub OAuth URL with correct parameters for login', async () => { - process.env.REACT_APP_GITHUB_CLIENT_ID = 'test-github-client-id'; + (config as any).githubClientId = 'test-github-client-id'; await initiateOAuth('github', 'login', { redirectUri: 'http://localhost:3000/auth/callback', @@ -213,7 +229,7 @@ describe('OAuth utility functions', () => { }); it('should redirect to GitHub OAuth URL with extended scope for signup', async () => { - process.env.REACT_APP_GITHUB_CLIENT_ID = 'test-github-client-id'; + (config as any).githubClientId = 'test-github-client-id'; await initiateOAuth('github', 'signup', { redirectUri: 'http://localhost:3000/auth/callback', @@ -224,7 +240,7 @@ describe('OAuth utility functions', () => { }); it('should store OAuth state in sessionStorage', async () => { - process.env.REACT_APP_GOOGLE_CLIENT_ID = 'test-google-client-id'; + (config as any).googleClientId = 'test-google-client-id'; await initiateOAuth('google', 'login', { redirectUri: 'http://localhost:3000/auth/callback', @@ -324,7 +340,7 @@ describe('OAuth utility functions', () => { }); it('should use REACT_APP_API_BASE_URL if apiBaseUrl is not provided', async () => { - process.env.REACT_APP_API_BASE_URL = 'http://localhost:4000'; + (config as any).apiBaseUrl = 'http://localhost:4000'; global.fetch = jest.fn().mockResolvedValue({ ok: true, diff --git a/src/utils/oauth.ts b/src/utils/oauth.ts index 1bd4eac..596d97c 100644 --- a/src/utils/oauth.ts +++ b/src/utils/oauth.ts @@ -1,6 +1,7 @@ /** * OAuth utility functions for Google and GitHub authentication */ +import { config as envConfig } from '../config/env'; export type OAuthProvider = 'google' | 'github'; @@ -167,8 +168,8 @@ export const initiateOAuth = ( // Get provider-specific client ID const clientId = provider === 'google' - ? config.googleClientId || process.env.REACT_APP_GOOGLE_CLIENT_ID - : config.githubClientId || process.env.REACT_APP_GITHUB_CLIENT_ID; + ? config.googleClientId || envConfig.googleClientId + : config.githubClientId || envConfig.githubClientId; if (!clientId) { const providerName = provider === 'google' ? 'Google' : 'GitHub'; @@ -218,8 +219,7 @@ export const handleOAuthCallback = async ( } const { provider, type } = stateData; - const apiBaseUrl = - config.apiBaseUrl || process.env.REACT_APP_API_BASE_URL || ''; + const apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl || ''; // Exchange code for token via backend API const response = await fetch( @@ -283,10 +283,10 @@ export const handleOAuthCallback = async ( * Checks if OAuth provider is configured */ export const isOAuthProviderConfigured = (provider: OAuthProvider): boolean => { - // Keep in sync with initiateOAuth: only use REACT_APP_GOOGLE_CLIENT_ID + // Keep in sync with initiateOAuth: only use Google client ID from config if (provider === 'google') { - return !!process.env.REACT_APP_GOOGLE_CLIENT_ID; + return !!envConfig.googleClientId; } - // Keep in sync with initiateOAuth: only use REACT_APP_GITHUB_CLIENT_ID - return !!process.env.REACT_APP_GITHUB_CLIENT_ID; + // Keep in sync with initiateOAuth: only use GitHub client ID from config + return !!envConfig.githubClientId; }; diff --git a/src/utils/urlUtils.ts b/src/utils/urlUtils.ts index 4a68439..a0bfcc9 100644 --- a/src/utils/urlUtils.ts +++ b/src/utils/urlUtils.ts @@ -1,6 +1,4 @@ -/** - * Utility functions for URL handling - */ +import { config } from '../config/env'; /** * Gets the base URL for the current environment @@ -17,9 +15,7 @@ export const getBaseUrl = (): string => { const port = window.location.port ? `:${window.location.port}` : ''; // Check if we're in local development with auth enabled - const enableLocalAuth = - process.env.REACT_APP_ENABLE_LOCAL_AUTH === 'true' || - process.env.REACT_APP_ENABLE_LOCAL_AUTH === '1'; + const { enableLocalAuth } = config; // If local auth is enabled and we're on localhost, use localhost if ( @@ -46,6 +42,6 @@ export const getBaseUrl = (): string => { * Uses REACT_APP_API_BASE_URL if set, otherwise uses relative paths */ export const getApiBaseUrl = (): string => { - const url = process.env.REACT_APP_API_BASE_URL || ''; + const url = config.apiBaseUrl || ''; return url.endsWith('/') ? url.slice(0, -1) : url; };