);
};
/**
* 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();
const { canUse } = usePlanFeatures();
// Apply dark mode class and persist to localStorage
React.useEffect(() => {
document.documentElement.classList.toggle('dark', darkMode);
localStorage.setItem('darkMode', JSON.stringify(darkMode));
}, [darkMode]);
// Set noindex/nofollow for app subdomains (platform, business subdomains)
// Only the root domain marketing pages should be indexed
React.useEffect(() => {
const hostname = window.location.hostname;
const parts = hostname.split('.');
const hasSubdomain = parts.length > 2 || (parts.length === 2 && parts[0] !== 'localhost');
// Check if we're on a subdomain (platform.*, demo.*, etc.)
const isSubdomain = hostname !== 'localhost' && hostname !== '127.0.0.1' && parts.length > 2;
if (isSubdomain) {
// Always noindex/nofollow on subdomains (app areas)
let metaRobots = document.querySelector('meta[name="robots"]');
if (metaRobots) {
metaRobots.setAttribute('content', 'noindex, nofollow');
} else {
metaRobots = document.createElement('meta');
metaRobots.setAttribute('name', 'robots');
metaRobots.setAttribute('content', 'noindex, nofollow');
document.head.appendChild(metaRobots);
}
}
}, []);
// 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, handle logged-in users appropriately
if (isRootDomain()) {
// If user is logged in as a business user (owner, staff, resource), redirect to their tenant dashboard
if (user) {
const isBusinessUserOnRoot = ['owner', 'staff', 'resource'].includes(user.role);
const isCustomerOnRoot = user.role === 'customer';
const hostname = window.location.hostname;
const parts = hostname.split('.');
const baseDomain = parts.length >= 2 ? parts.slice(-2).join('.') : hostname;
const port = window.location.port ? `:${window.location.port}` : '';
const protocol = window.location.protocol;
// Business users on root domain: redirect to their tenant dashboard
if (isBusinessUserOnRoot && user.business_subdomain) {
window.location.href = `${protocol}//${user.business_subdomain}.${baseDomain}${port}/dashboard`;
return ;
}
// Customers on root domain: log them out and show the form
// Customers should only access their business subdomain
if (isCustomerOnRoot) {
deleteCookie('access_token');
deleteCookie('refresh_token');
localStorage.removeItem('masquerade_stack');
// Don't redirect, just let them see the page as unauthenticated
window.location.reload();
return ;
}
}
// Show marketing site for unauthenticated users and platform users (who should use platform subdomain)
return (
}>
}>
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
);
}
// Not authenticated - show appropriate page based on subdomain
if (!user) {
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';
const isPlatformSubdomain = hostnameParts[0] === 'platform';
const currentSubdomain = hostnameParts[0];
// Check if we're on a business subdomain (not root, not platform, not api)
const isBusinessSubdomain = !isRootDomainForUnauthUser && !isPlatformSubdomain && currentSubdomain !== 'api';
// For business subdomains, show the tenant landing page with login option
if (isBusinessSubdomain) {
return (
}>
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
);
}
// For platform subdomain, only specific paths exist - everything else renders nothing
if (isPlatformSubdomain) {
const path = window.location.pathname;
const allowedPaths = ['/platform/login', '/mfa-verify', '/verify-email', '/platform-staff-invite'];
// If not an allowed path, render nothing
if (!allowedPaths.includes(path)) {
return null;
}
return (
}>
} />
} />
} />
} />
);
}
// For root domain, show marketing site with business user login
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', '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: Non-platform users on platform subdomain should have their session cleared
// This handles cases where masquerading changed tokens to a business user
if (!isPlatformUser && isPlatformDomain) {
deleteCookie('access_token');
deleteCookie('refresh_token');
localStorage.removeItem('masquerade_stack');
window.location.href = '/platform/login';
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 only access their own business subdomain
// If on platform domain or wrong business subdomain, log them out and let them use the form
if (isCustomer && isPlatformDomain) {
deleteCookie('access_token');
deleteCookie('refresh_token');
localStorage.removeItem('masquerade_stack');
window.location.reload();
return ;
}
if (isCustomer && isBusinessSubdomain && user.business_subdomain && user.business_subdomain !== currentSubdomain) {
// Customer is on a different business's subdomain - log them out
// They might be trying to book with a different business
deleteCookie('access_token');
deleteCookie('refresh_token');
localStorage.removeItem('masquerade_stack');
window.location.reload();
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);
// Helper to check permission-based access (owner always has access, staff uses effective_permissions)
const canAccess = (permissionKey: string): boolean => {
if (user.role === 'owner') return true;
if (user.role === 'staff') {
return user.effective_permissions?.[permissionKey] === true;
}
return false;
};
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, staff, resource)
if (['owner', '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 = ['/dashboard/trial-expired', '/dashboard/upgrade', '/dashboard/settings', '/dashboard/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 (
}>
{/* Public routes outside BusinessLayout */}
} />
} />
} />
{/* Logged-in business users on their own subdomain get redirected to dashboard */}
} />
} />
{/* Point of Sale - Full screen mode outside BusinessLayout */}
) : (
)
}
/>
{/* Dashboard routes inside BusinessLayout */}
}
>
{/* Trial and Upgrade Routes */}
} />
} />
{/* Regular Routes */}
: user.role === 'staff' ? : }
/>
{/* Staff Schedule - vertical timeline view */}
) : (
)
}
/>
} />
} />
) : (
)
}
/>
} />
} />
} />
} />
} />
{/* New help pages */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Automations - Activepieces embedded builder */}
) : (
)
}
/>
{/* Redirect old automation routes to new page */}
} />
} />
} />
} />
{/* Email templates are now accessed via Settings > Email Templates */}
} />
) : (
)
}
/>
{/* Redirect old services path to new settings location */}
}
/>
) : (
)
}
/>
) : (
)
}
/>
) : (
)
}
/>
{/* Redirect old locations path to new settings location */}
}
/>
) : (
)
}
/>
) : (
)
}
/>
) : (
)
}
/>
:
}
/>
) : (
)
}
/>
{/* Redirect old site-editor path to new settings location */}
}
/>
) : (
)
}
/>
) : (
)
}
/>
{/* Products Management */}
) : (
)
}
/>
{/* Settings Routes with Nested Layout */}
{/* Owners have full access, staff need can_access_settings permission */}
{canAccess('can_access_settings') ? (
}>
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
{/* Moved from main sidebar */}
} />
} />
} />
) : (
} />
)}
} />
} />
{/* Catch-all redirects to home */}
} />
);
}
// Fallback
return ;
};
/**
* Main App Component
*/
const App: React.FC = () => {
return (
{/* Add Toaster component for notifications */}
);
};
export default App;