/** * Main App Component - Integrated with Real API */ import React, { useState, Suspense } from 'react'; import { useTranslation } from 'react-i18next'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useCurrentUser, useMasquerade, useLogout } from './hooks/useAuth'; import { useCurrentBusiness } from './hooks/useBusiness'; import { useUpdateBusiness } from './hooks/useBusiness'; import { setCookie } from './utils/cookies'; // Import Login Page const LoginPage = React.lazy(() => import('./pages/LoginPage')); const MFAVerifyPage = React.lazy(() => import('./pages/MFAVerifyPage')); const OAuthCallback = React.lazy(() => import('./pages/OAuthCallback')); // Import layouts import BusinessLayout from './layouts/BusinessLayout'; import PlatformLayout from './layouts/PlatformLayout'; import CustomerLayout from './layouts/CustomerLayout'; import MarketingLayout from './layouts/MarketingLayout'; // Import marketing pages const HomePage = React.lazy(() => import('./pages/marketing/HomePage')); const FeaturesPage = React.lazy(() => import('./pages/marketing/FeaturesPage')); const PricingPage = React.lazy(() => import('./pages/marketing/PricingPage')); const AboutPage = React.lazy(() => import('./pages/marketing/AboutPage')); const ContactPage = React.lazy(() => import('./pages/marketing/ContactPage')); const SignupPage = React.lazy(() => import('./pages/marketing/SignupPage')); const PrivacyPolicyPage = React.lazy(() => import('./pages/marketing/PrivacyPolicyPage')); const TermsOfServicePage = React.lazy(() => import('./pages/marketing/TermsOfServicePage')); // Import pages const Dashboard = React.lazy(() => import('./pages/Dashboard')); const Scheduler = React.lazy(() => import('./pages/Scheduler')); const Customers = React.lazy(() => import('./pages/Customers')); const Settings = React.lazy(() => import('./pages/Settings')); const Payments = React.lazy(() => import('./pages/Payments')); const Resources = React.lazy(() => import('./pages/Resources')); const Services = React.lazy(() => import('./pages/Services')); const Staff = React.lazy(() => import('./pages/Staff')); const CustomerDashboard = React.lazy(() => import('./pages/customer/CustomerDashboard')); const CustomerSupport = React.lazy(() => import('./pages/customer/CustomerSupport')); const ResourceDashboard = React.lazy(() => import('./pages/resource/ResourceDashboard')); const BookingPage = React.lazy(() => import('./pages/customer/BookingPage')); const TrialExpired = React.lazy(() => import('./pages/TrialExpired')); const Upgrade = React.lazy(() => import('./pages/Upgrade')); // Import platform pages const PlatformDashboard = React.lazy(() => import('./pages/platform/PlatformDashboard')); const PlatformBusinesses = React.lazy(() => import('./pages/platform/PlatformBusinesses')); const PlatformSupportPage = React.lazy(() => import('./pages/platform/PlatformSupport')); const PlatformEmailAddresses = React.lazy(() => import('./pages/platform/PlatformEmailAddresses')); const PlatformUsers = React.lazy(() => import('./pages/platform/PlatformUsers')); const PlatformStaff = React.lazy(() => import('./pages/platform/PlatformStaff')); const PlatformSettings = React.lazy(() => import('./pages/platform/PlatformSettings')); const ProfileSettings = React.lazy(() => import('./pages/ProfileSettings')); const VerifyEmail = React.lazy(() => import('./pages/VerifyEmail')); const EmailVerificationRequired = React.lazy(() => import('./pages/EmailVerificationRequired')); const AcceptInvitePage = React.lazy(() => import('./pages/AcceptInvitePage')); const TenantOnboardPage = React.lazy(() => import('./pages/TenantOnboardPage')); const Tickets = React.lazy(() => import('./pages/Tickets')); // Import Tickets page const HelpGuide = React.lazy(() => import('./pages/HelpGuide')); // Import Platform Guide page const HelpTicketing = React.lazy(() => import('./pages/HelpTicketing')); // Import Help page for ticketing const HelpApiDocs = React.lazy(() => import('./pages/HelpApiDocs')); // Import API documentation page const HelpPluginDocs = React.lazy(() => import('./pages/help/HelpPluginDocs')); // Import Plugin documentation page const HelpEmailSettings = React.lazy(() => import('./pages/HelpEmailSettings')); // Import Email settings help page // Import new help pages const HelpDashboard = React.lazy(() => import('./pages/help/HelpDashboard')); const HelpScheduler = React.lazy(() => import('./pages/help/HelpScheduler')); const HelpTasks = React.lazy(() => import('./pages/help/HelpTasks')); const HelpCustomers = React.lazy(() => import('./pages/help/HelpCustomers')); const HelpServices = React.lazy(() => import('./pages/help/HelpServices')); const HelpResources = React.lazy(() => import('./pages/help/HelpResources')); const HelpStaff = React.lazy(() => import('./pages/help/HelpStaff')); const HelpMessages = React.lazy(() => import('./pages/help/HelpMessages')); const HelpPayments = React.lazy(() => import('./pages/help/HelpPayments')); const HelpPlugins = React.lazy(() => import('./pages/help/HelpPlugins')); const HelpSettingsGeneral = React.lazy(() => import('./pages/help/HelpSettingsGeneral')); const HelpSettingsResourceTypes = React.lazy(() => import('./pages/help/HelpSettingsResourceTypes')); const HelpSettingsBooking = React.lazy(() => import('./pages/help/HelpSettingsBooking')); const HelpSettingsAppearance = React.lazy(() => import('./pages/help/HelpSettingsAppearance')); const HelpSettingsEmail = React.lazy(() => import('./pages/help/HelpSettingsEmail')); const HelpSettingsDomains = React.lazy(() => import('./pages/help/HelpSettingsDomains')); const HelpSettingsApi = React.lazy(() => import('./pages/help/HelpSettingsApi')); const HelpSettingsAuth = React.lazy(() => import('./pages/help/HelpSettingsAuth')); const HelpSettingsBilling = React.lazy(() => import('./pages/help/HelpSettingsBilling')); const HelpSettingsQuota = React.lazy(() => import('./pages/help/HelpSettingsQuota')); const HelpComprehensive = React.lazy(() => import('./pages/help/HelpComprehensive')); const PlatformSupport = React.lazy(() => import('./pages/PlatformSupport')); // Import Platform Support page (for businesses to contact SmoothSchedule) const PluginMarketplace = React.lazy(() => import('./pages/PluginMarketplace')); // Import Plugin Marketplace page const MyPlugins = React.lazy(() => import('./pages/MyPlugins')); // Import My Plugins page const CreatePlugin = React.lazy(() => import('./pages/CreatePlugin')); // Import Create Plugin page const Tasks = React.lazy(() => import('./pages/Tasks')); // Import Tasks page for scheduled plugin executions const EmailTemplates = React.lazy(() => import('./pages/EmailTemplates')); // Import Email Templates page // Settings pages const SettingsLayout = React.lazy(() => import('./layouts/SettingsLayout')); const GeneralSettings = React.lazy(() => import('./pages/settings/GeneralSettings')); const BrandingSettings = React.lazy(() => import('./pages/settings/BrandingSettings')); const ResourceTypesSettings = React.lazy(() => import('./pages/settings/ResourceTypesSettings')); const BookingSettings = React.lazy(() => import('./pages/settings/BookingSettings')); const CustomDomainsSettings = React.lazy(() => import('./pages/settings/CustomDomainsSettings')); const ApiSettings = React.lazy(() => import('./pages/settings/ApiSettings')); const AuthenticationSettings = React.lazy(() => import('./pages/settings/AuthenticationSettings')); const EmailSettings = React.lazy(() => import('./pages/settings/EmailSettings')); const CommunicationSettings = React.lazy(() => import('./pages/settings/CommunicationSettings')); const BillingSettings = React.lazy(() => import('./pages/settings/BillingSettings')); const QuotaSettings = React.lazy(() => import('./pages/settings/QuotaSettings')); import { Toaster } from 'react-hot-toast'; // Import Toaster for notifications const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1, staleTime: 30000, // 30 seconds }, }, }); /** * Loading Component */ const LoadingScreen: React.FC = () => { const { t } = useTranslation(); return (

{t('common.loading')}

); }; /** * Error Component */ const ErrorScreen: React.FC<{ error: Error }> = ({ error }) => { const { t } = useTranslation(); return (

{t('common.error')}

{error.message}

); }; /** * App Content - Handles routing based on auth state */ const AppContent: React.FC = () => { // Check for tokens in URL FIRST - before any queries execute // This handles login/masquerade redirects that pass tokens in the URL const [processingUrlTokens] = useState(() => { const params = new URLSearchParams(window.location.search); return !!(params.get('access_token') && params.get('refresh_token')); }); const { data: user, isLoading: userLoading, error: userError } = useCurrentUser(); const { data: business, isLoading: businessLoading, error: businessError } = useCurrentBusiness(); const [darkMode, setDarkMode] = useState(() => { // Check localStorage first, then system preference const saved = localStorage.getItem('darkMode'); if (saved !== null) { return JSON.parse(saved); } return window.matchMedia('(prefers-color-scheme: dark)').matches; }); const updateBusinessMutation = useUpdateBusiness(); const masqueradeMutation = useMasquerade(); const logoutMutation = useLogout(); // Apply dark mode class and persist to localStorage React.useEffect(() => { document.documentElement.classList.toggle('dark', darkMode); localStorage.setItem('darkMode', JSON.stringify(darkMode)); }, [darkMode]); // Handle tokens in URL (from login or masquerade redirect) React.useEffect(() => { const params = new URLSearchParams(window.location.search); const accessToken = params.get('access_token'); const refreshToken = params.get('refresh_token'); if (accessToken && refreshToken) { // Extract masquerade stack if present (for masquerade banner) const masqueradeStackParam = params.get('masquerade_stack'); if (masqueradeStackParam) { try { const masqueradeStack = JSON.parse(decodeURIComponent(masqueradeStackParam)); localStorage.setItem('masquerade_stack', JSON.stringify(masqueradeStack)); } catch (e) { console.error('Failed to parse masquerade stack', e); } } // For backward compatibility, also check for original_user parameter const originalUserParam = params.get('original_user'); if (originalUserParam && !masqueradeStackParam) { try { const originalUser = JSON.parse(decodeURIComponent(originalUserParam)); // Convert old format to new stack format (single entry) const stack = [{ user_id: originalUser.id, username: originalUser.username, role: originalUser.role, business_id: originalUser.business, business_subdomain: originalUser.business_subdomain, }]; localStorage.setItem('masquerade_stack', JSON.stringify(stack)); } catch (e) { console.error('Failed to parse original user', e); } } // Set cookies using helper (handles domain correctly) setCookie('access_token', accessToken, 7); setCookie('refresh_token', refreshToken, 7); // Clean URL const newUrl = window.location.pathname + window.location.hash; window.history.replaceState({}, '', newUrl); // Force reload to ensure auth state is picked up window.location.reload(); } }, []); // Show loading while processing URL tokens (before reload happens) if (processingUrlTokens) { return ; } // Loading state if (userLoading) { return ; } // Helper to detect root domain (for marketing site) const isRootDomain = (): boolean => { const hostname = window.location.hostname; // Root domain has no subdomain (just the base domain like smoothschedule.com or lvh.me) const parts = hostname.split('.'); return hostname === 'localhost' || hostname === '127.0.0.1' || parts.length === 2; }; // On root domain, ALWAYS show marketing site (even if logged in) // Logged-in users will see a "Go to Dashboard" link in the navbar if (isRootDomain()) { return ( }> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } // Not authenticated - redirect to root domain for login if on subdomain if (!user) { // If on a subdomain, redirect to root domain login page const currentHostname = window.location.hostname; const hostnameParts = currentHostname.split('.'); const baseDomain = hostnameParts.length >= 2 ? hostnameParts.slice(-2).join('.') : currentHostname; const isRootDomainForUnauthUser = currentHostname === baseDomain || currentHostname === 'localhost'; if (!isRootDomainForUnauthUser) { // Redirect to root domain login (preserve port) const protocol = window.location.protocol; const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `${protocol}//${baseDomain}${port}/login`; return ; } return ( }> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } // Error state if (userError) { return ; } // Subdomain validation for logged-in users const currentHostname = window.location.hostname; const hostnameParts = currentHostname.split('.'); const baseDomain = hostnameParts.length >= 2 ? hostnameParts.slice(-2).join('.') : currentHostname; const protocol = window.location.protocol; const isPlatformDomain = currentHostname === `platform.${baseDomain}`; const currentSubdomain = hostnameParts[0]; const isBusinessSubdomain = !isRootDomain() && !isPlatformDomain && currentSubdomain !== 'api' && currentHostname !== baseDomain; const isPlatformUser = ['superuser', 'platform_manager', 'platform_support'].includes(user.role); const isBusinessUser = ['owner', 'manager', 'staff', 'resource'].includes(user.role); const isCustomer = user.role === 'customer'; // RULE: Platform users on business subdomains should be redirected to platform subdomain if (isPlatformUser && isBusinessSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `${protocol}//platform.${baseDomain}${port}/`; return ; } // RULE: Business users must be on their own business subdomain if (isBusinessUser && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `${protocol}//${user.business_subdomain}.${baseDomain}${port}/`; return ; } // RULE: Customers must be on their business subdomain if (isCustomer && isPlatformDomain && user.business_subdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `${protocol}//${user.business_subdomain}.${baseDomain}${port}/`; return ; } if (isCustomer && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) { const port = window.location.port ? `:${window.location.port}` : ''; window.location.href = `${protocol}//${user.business_subdomain}.${baseDomain}${port}/`; return ; } // Handlers const toggleTheme = () => setDarkMode((prev) => !prev); const handleSignOut = () => { logoutMutation.mutate(); }; const handleUpdateBusiness = (updates: Partial) => { updateBusinessMutation.mutate(updates); }; const handleMasquerade = (targetUser: any) => { // Call the masquerade API with the target user's id const userId = targetUser.id; if (!userId) { console.error('Cannot masquerade: no user id available', targetUser); return; } // Ensure userId is a number const userPk = typeof userId === 'string' ? parseInt(userId, 10) : userId; masqueradeMutation.mutate(userPk); }; // Helper to check access based on roles const hasAccess = (allowedRoles: string[]) => allowedRoles.includes(user.role); if (isPlatformUser) { return ( }> } > {(user.role === 'superuser' || user.role === 'platform_manager') && ( <> } /> } /> } /> } /> )} } /> } /> } /> } /> } /> } /> } /> {user.role === 'superuser' && ( } /> )} } /> } /> } /> ); } // Customer users if (user.role === 'customer') { // Wait for business data to load if (businessLoading) { return ; } // Handle business not found for customers if (!business) { return (

Business Not Found

Unable to load business data. Please try again.

); } return ( }> } > } /> } /> } /> } /> } /> } /> } /> ); } // Business loading - show loading with user info if (businessLoading) { return ; } // Business error or no business found if (businessError || !business) { // If user has a business subdomain, redirect them there if (user.business_subdomain) { window.location.href = buildSubdomainUrl(user.business_subdomain, '/'); return ; } // No business subdomain - show error return (

Business Not Found

{businessError instanceof Error ? businessError.message : 'Unable to load business data. Please check your subdomain or try again.'}

); } // Business users (owner, manager, staff, resource) if (['owner', 'manager', 'staff', 'resource'].includes(user.role)) { // Check if email verification is required if (!user.email_verified) { return ( }> } /> } /> } /> ); } // Check if trial has expired const isTrialExpired = business.isTrialExpired || (business.status === 'Trial' && business.trialEnd && new Date(business.trialEnd) < new Date()); // Allowed routes when trial is expired const allowedWhenExpired = ['/trial-expired', '/upgrade', '/settings', '/profile']; const currentPath = window.location.pathname; const isOnAllowedRoute = allowedWhenExpired.some(route => currentPath.startsWith(route)); // If trial expired and not on allowed route, redirect to trial-expired if (isTrialExpired && !isOnAllowedRoute) { return ( }> } /> } /> } /> {/* Trial-expired users can access billing settings to upgrade */} : } /> } /> ); } return ( }> } > {/* Trial and Upgrade Routes */} } /> } /> {/* Regular Routes */} : } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* New help pages */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> ) : ( ) } /> : } />

Messages

Messages feature coming soon...

) : ( ) } /> {/* Settings Routes with Nested Layout */} {hasAccess(['owner']) ? ( }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ) : ( } /> )} } /> } /> } />
); } // Fallback return ; }; /** * Main App Component */ const App: React.FC = () => { return ( {/* Add Toaster component for notifications */} ); }; export default App;