- Update PricingPage success_url to point to podcast-maker in demo mode - Handle ?subscription=success query param in InitialRouteHandler
688 lines
29 KiB
TypeScript
688 lines
29 KiB
TypeScript
import React from 'react';
|
|
import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|
import { Box, CircularProgress, Typography } from '@mui/material';
|
|
import { CopilotKit } from "@copilotkit/react-core";
|
|
import { ClerkProvider, useAuth } from '@clerk/clerk-react';
|
|
import "@copilotkit/react-ui/styles.css";
|
|
import { shouldSkipOnboarding } from './utils/demoMode';
|
|
import Wizard from './components/OnboardingWizard/Wizard';
|
|
import MainDashboard from './components/MainDashboard/MainDashboard';
|
|
import SEODashboard from './components/SEODashboard/SEODashboard';
|
|
import ContentPlanningDashboard from './components/ContentPlanningDashboard/ContentPlanningDashboard';
|
|
import FacebookWriter from './components/FacebookWriter/FacebookWriter';
|
|
import LinkedInWriter from './components/LinkedInWriter/LinkedInWriter';
|
|
import BlogWriter from './components/BlogWriter/BlogWriter';
|
|
import StoryWriter from './components/StoryWriter/StoryWriter';
|
|
import { StoryProjectList } from './components/StoryWriter/StoryProjectList';
|
|
import YouTubeCreator from './components/YouTubeCreator/YouTubeCreator';
|
|
import { CreateStudio, EditStudio, UpscaleStudio, ControlStudio, SocialOptimizer, AssetLibrary, ImageStudioDashboard, FaceSwapStudio, CompressionStudio, ImageProcessingStudio } from './components/ImageStudio';
|
|
import {
|
|
VideoStudioDashboard,
|
|
CreateVideo,
|
|
AvatarVideo,
|
|
EnhanceVideo,
|
|
ExtendVideo,
|
|
EditVideo,
|
|
TransformVideo,
|
|
SocialVideo,
|
|
FaceSwap,
|
|
VideoTranslate,
|
|
VideoBackgroundRemover,
|
|
AddAudioToVideo,
|
|
LibraryVideo,
|
|
} from './components/VideoStudio';
|
|
import {
|
|
ProductMarketingDashboard,
|
|
ProductPhotoshootStudio,
|
|
ProductAnimationStudio,
|
|
ProductVideoStudio,
|
|
ProductAvatarStudio,
|
|
} from './components/ProductMarketing';
|
|
import PodcastDashboard from './components/PodcastMaker/PodcastDashboard';
|
|
import PricingPage from './components/Pricing/PricingPage';
|
|
import WixTestPage from './components/WixTestPage/WixTestPage';
|
|
import WixCallbackPage from './components/WixCallbackPage/WixCallbackPage';
|
|
import WordPressCallbackPage from './components/WordPressCallbackPage/WordPressCallbackPage';
|
|
import BingCallbackPage from './components/BingCallbackPage/BingCallbackPage';
|
|
import BingAnalyticsStorage from './components/BingAnalyticsStorage/BingAnalyticsStorage';
|
|
import ResearchDashboard from './pages/ResearchDashboard';
|
|
import IntentResearchTest from './pages/IntentResearchTest';
|
|
import SchedulerDashboard from './pages/SchedulerDashboard';
|
|
import BillingPage from './pages/BillingPage';
|
|
import ApprovalsPage from './pages/ApprovalsPage';
|
|
import TeamActivityPage from './pages/TeamActivityPage';
|
|
import StripeDisputesDashboard from './pages/StripeDisputesDashboard';
|
|
import ProtectedRoute from './components/shared/ProtectedRoute';
|
|
import GSCAuthCallback from './components/SEODashboard/components/GSCAuthCallback';
|
|
import Landing from './components/Landing/Landing';
|
|
import ErrorBoundary from './components/shared/ErrorBoundary';
|
|
import ErrorBoundaryTest from './components/shared/ErrorBoundaryTest';
|
|
import CopilotKitDegradedBanner from './components/shared/CopilotKitDegradedBanner';
|
|
import { OnboardingProvider } from './contexts/OnboardingContext';
|
|
import { SubscriptionProvider, useSubscription } from './contexts/SubscriptionContext';
|
|
import { CopilotKitHealthProvider } from './contexts/CopilotKitHealthContext';
|
|
import { useOAuthTokenAlerts } from './hooks/useOAuthTokenAlerts';
|
|
|
|
import { setAuthTokenGetter, setClerkSignOut } from './api/client';
|
|
import { setMediaAuthTokenGetter } from './utils/fetchMediaBlobUrl';
|
|
import { setBillingAuthTokenGetter } from './services/billingService';
|
|
import { useOnboarding } from './contexts/OnboardingContext';
|
|
import { useState, useEffect } from 'react';
|
|
import ConnectionErrorPage from './components/shared/ConnectionErrorPage';
|
|
|
|
// interface OnboardingStatus {
|
|
// onboarding_required: boolean;
|
|
// onboarding_complete: boolean;
|
|
// current_step?: number;
|
|
// total_steps?: number;
|
|
// completion_percentage?: number;
|
|
// }
|
|
|
|
// Conditional CopilotKit wrapper that only shows sidebar on content-planning route
|
|
const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
// Do not render CopilotSidebar here. Let specific pages/components control it.
|
|
return <>{children}</>;
|
|
};
|
|
|
|
// Wrapper to only enable CopilotKit checks/provider when user is authenticated
|
|
// This prevents CopilotKit from running on the Landing page
|
|
const AuthenticatedCopilotWrapper: React.FC<{
|
|
children: React.ReactNode;
|
|
apiKey: string;
|
|
}> = ({ children, apiKey }) => {
|
|
const { isSignedIn } = useAuth();
|
|
const location = useLocation();
|
|
|
|
// Exclude CopilotKit from running on:
|
|
// 1. Landing page (handled by !isSignedIn)
|
|
// 2. Onboarding pages (to prevent health check timeouts)
|
|
const shouldExcludeCopilot = !isSignedIn || location.pathname.startsWith('/onboarding');
|
|
|
|
if (shouldExcludeCopilot) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
const hasKey = apiKey && apiKey.trim();
|
|
|
|
if (hasKey) {
|
|
// Enhanced error handler that updates health context
|
|
const handleCopilotKitError = (e: any) => {
|
|
console.error("CopilotKit Error:", e);
|
|
|
|
// Try to get health context if available
|
|
// We'll use a custom event to notify health context since we can't access it directly here
|
|
const errorMessage = e?.error?.message || e?.message || 'CopilotKit error occurred';
|
|
const errorType = errorMessage.toLowerCase();
|
|
|
|
// Differentiate between fatal and transient errors
|
|
const isFatalError =
|
|
errorType.includes('cors') ||
|
|
errorType.includes('ssl') ||
|
|
errorType.includes('certificate') ||
|
|
errorType.includes('403') ||
|
|
errorType.includes('forbidden') ||
|
|
errorType.includes('ERR_CERT_COMMON_NAME_INVALID');
|
|
|
|
// Dispatch event for health context to listen to
|
|
window.dispatchEvent(new CustomEvent('copilotkit-error', {
|
|
detail: {
|
|
error: e,
|
|
errorMessage,
|
|
isFatal: isFatalError,
|
|
}
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<CopilotKitHealthProvider initialHealthStatus={true}>
|
|
<CopilotKitDegradedBanner />
|
|
<ErrorBoundary
|
|
context="CopilotKit"
|
|
showDetails={process.env.NODE_ENV === 'development'}
|
|
fallback={
|
|
<Box sx={{ p: 3, textAlign: 'center' }}>
|
|
<Typography variant="h6" color="warning" gutterBottom>
|
|
Chat Unavailable
|
|
</Typography>
|
|
<Typography variant="body2" color="textSecondary">
|
|
CopilotKit encountered an error. The app continues to work with manual controls.
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
>
|
|
<CopilotKit
|
|
publicApiKey={apiKey}
|
|
showDevConsole={false}
|
|
onError={handleCopilotKitError}
|
|
>
|
|
{children}
|
|
</CopilotKit>
|
|
</ErrorBoundary>
|
|
</CopilotKitHealthProvider>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<CopilotKitHealthProvider initialHealthStatus={false}>
|
|
<CopilotKitDegradedBanner />
|
|
{children}
|
|
</CopilotKitHealthProvider>
|
|
);
|
|
};
|
|
|
|
// Component to handle initial routing based on subscription and onboarding status
|
|
// Flow: Subscription → Onboarding → Dashboard
|
|
const InitialRouteHandler: React.FC = () => {
|
|
const { loading, error, isOnboardingComplete, initializeOnboarding, data } = useOnboarding();
|
|
const { subscription, loading: subscriptionLoading, checkSubscription } = useSubscription();
|
|
const location = useLocation();
|
|
const [connectionError, setConnectionError] = useState<{
|
|
hasError: boolean;
|
|
error: Error | null;
|
|
}>({
|
|
hasError: false,
|
|
error: null,
|
|
});
|
|
|
|
// Poll for OAuth token alerts and show toast notifications
|
|
// Only enabled when user is authenticated (has subscription)
|
|
useOAuthTokenAlerts({
|
|
enabled: subscription?.active === true,
|
|
interval: 60000, // Poll every 1 minute
|
|
});
|
|
|
|
// Check subscription on mount (non-blocking - don't wait for it to route)
|
|
useEffect(() => {
|
|
// Delay subscription check slightly to allow auth token getter to be installed first
|
|
const timeoutId = setTimeout(async () => {
|
|
// Retry logic for initial subscription check
|
|
const maxRetries = 3;
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
await checkSubscription();
|
|
break; // Success
|
|
} catch (err) {
|
|
console.error(`App: Subscription check attempt ${attempt + 1} failed:`, err);
|
|
|
|
// If it's a connection error and we have retries left, wait and retry
|
|
const isConnectionError = err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError');
|
|
|
|
if (isConnectionError && attempt < maxRetries - 1) {
|
|
const delay = 1000 * Math.pow(2, attempt); // 1s, 2s
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
continue;
|
|
}
|
|
|
|
// If final attempt or not a connection error, handle it
|
|
if (attempt === maxRetries - 1 || !isConnectionError) {
|
|
if (isConnectionError) {
|
|
setConnectionError({
|
|
hasError: true,
|
|
error: err as Error,
|
|
});
|
|
}
|
|
// Don't block routing on other errors
|
|
}
|
|
}
|
|
}
|
|
}, 100); // Small delay to ensure TokenInstaller has run
|
|
|
|
return () => clearTimeout(timeoutId);
|
|
}, []); // Remove checkSubscription dependency to prevent loop
|
|
|
|
// Initialize onboarding only after subscription is confirmed
|
|
useEffect(() => {
|
|
if (subscription && !subscriptionLoading) {
|
|
// Check if user is new (no subscription record at all)
|
|
const isNewUser = !subscription || subscription.plan === 'none';
|
|
|
|
console.log('InitialRouteHandler: Subscription data received:', {
|
|
plan: subscription.plan,
|
|
active: subscription.active,
|
|
isNewUser,
|
|
subscriptionLoading
|
|
});
|
|
|
|
if (subscription.active && !isNewUser) {
|
|
console.log('InitialRouteHandler: Subscription confirmed, initializing onboarding...');
|
|
|
|
// Handle post-Stripe-checkout redirect in demo mode
|
|
const urlParams = new URLSearchParams(location.search);
|
|
if (urlParams.get('subscription') === 'success' && shouldSkipOnboarding()) {
|
|
console.log('InitialRouteHandler: Stripe checkout success in demo mode → Podcast Maker');
|
|
return <Navigate to="/podcast-maker" replace />;
|
|
}
|
|
|
|
initializeOnboarding();
|
|
}
|
|
}
|
|
}, [subscription, subscriptionLoading, initializeOnboarding, location.search]);
|
|
|
|
// Handle connection error - show connection error page
|
|
if (connectionError.hasError) {
|
|
const handleRetry = () => {
|
|
setConnectionError({
|
|
hasError: false,
|
|
error: null,
|
|
});
|
|
// Re-trigger the subscription check using context
|
|
checkSubscription().catch((err) => {
|
|
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
|
|
setConnectionError({
|
|
hasError: true,
|
|
error: err,
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleGoHome = () => {
|
|
window.location.href = '/';
|
|
};
|
|
|
|
return (
|
|
<ConnectionErrorPage
|
|
onRetry={handleRetry}
|
|
onGoHome={handleGoHome}
|
|
message={connectionError.error?.message || "Backend service is not available. Please check if the server is running."}
|
|
title="Connection Error"
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Loading state - only wait for onboarding init when user has active subscription
|
|
// In demo mode, skip waiting for onboarding data entirely
|
|
// This allows no-subscription/inactive flows to continue even when onboarding data is still null.
|
|
const isDemoMode = shouldSkipOnboarding();
|
|
const isActiveSubscriber = Boolean(subscription && subscription.active && subscription.plan !== 'none');
|
|
const waitingForOnboardingInit = !isDemoMode && isActiveSubscriber && (loading || !data);
|
|
if (waitingForOnboardingInit) {
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
flexDirection="column"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
minHeight="100vh"
|
|
gap={2}
|
|
>
|
|
<CircularProgress size={60} />
|
|
<Typography variant="h6" color="textSecondary">
|
|
Preparing your workspace...
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Error state
|
|
if (error) {
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
flexDirection="column"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
minHeight="100vh"
|
|
gap={2}
|
|
p={3}
|
|
>
|
|
<Typography variant="h5" color="error" gutterBottom>
|
|
Error
|
|
</Typography>
|
|
<Typography variant="body1" color="textSecondary" textAlign="center">
|
|
{error}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Decision tree for SIGNED-IN users:
|
|
// Priority: Subscription → Onboarding → Dashboard (as per user flow: Landing → Subscription → Onboarding → Dashboard)
|
|
|
|
// 1. If subscription is still loading, show loading state
|
|
if (subscriptionLoading) {
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
flexDirection="column"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
minHeight="100vh"
|
|
gap={2}
|
|
>
|
|
<CircularProgress size={60} />
|
|
<Typography variant="h6" color="textSecondary">
|
|
Checking subscription...
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// 2. No subscription data yet - handle gracefully
|
|
// If onboarding is complete, allow access to dashboard (user already went through flow)
|
|
// If onboarding not complete, check if subscription check is still loading or failed
|
|
if (!subscription) {
|
|
if (isOnboardingComplete) {
|
|
console.log('InitialRouteHandler: Onboarding complete but no subscription data → Dashboard (allow access)');
|
|
return <Navigate to="/dashboard" replace />;
|
|
}
|
|
|
|
// Onboarding not complete and no subscription data
|
|
// If subscription check is still loading, show loading state
|
|
if (subscriptionLoading) {
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
flexDirection="column"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
minHeight="100vh"
|
|
gap={2}
|
|
>
|
|
<CircularProgress size={60} />
|
|
<Typography variant="h6" color="textSecondary">
|
|
Checking subscription...
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Subscription check completed but returned null/undefined
|
|
// This likely means no subscription - redirect to pricing
|
|
console.log('InitialRouteHandler: No subscription data after check → Pricing page');
|
|
return <Navigate to="/pricing" replace />;
|
|
}
|
|
|
|
// 3. Check subscription status first
|
|
const isNewUser = !subscription || subscription.plan === 'none';
|
|
|
|
// No active subscription → Show modal (SubscriptionContext handles this)
|
|
// Don't redirect immediately - let the modal show first
|
|
// User can click "Renew Subscription" button in modal to go to pricing
|
|
// Or click "Maybe Later" to dismiss (but they still can't use features)
|
|
if (isNewUser || !subscription.active) {
|
|
console.log('InitialRouteHandler: No active subscription - modal will be shown by SubscriptionContext');
|
|
// Note: SubscriptionContext will show the modal automatically when subscription is inactive
|
|
// We still redirect to pricing for new users, but allow existing users with expired subscriptions
|
|
// to see the modal first. The modal has a "Renew Subscription" button that navigates to pricing.
|
|
// For new users (no subscription at all), redirect to pricing immediately
|
|
if (isNewUser) {
|
|
console.log('InitialRouteHandler: New user (no subscription) → Pricing page');
|
|
return <Navigate to="/pricing" replace />;
|
|
}
|
|
// For existing users with inactive subscription, show modal but don't redirect immediately
|
|
// The modal will be shown by SubscriptionContext, and user can click "Renew Subscription"
|
|
// Allow access to dashboard (modal will be shown and block functionality)
|
|
console.log('InitialRouteHandler: Inactive subscription - allowing access to show modal');
|
|
// Continue to onboarding/dashboard flow - modal will be shown by SubscriptionContext
|
|
}
|
|
|
|
// 4. Has active subscription, check onboarding status
|
|
if (!isOnboardingComplete) {
|
|
// In demo mode, skip onboarding and go directly to podcast-maker
|
|
if (shouldSkipOnboarding()) {
|
|
console.log('InitialRouteHandler: Demo mode - skipping onboarding → Podcast Maker');
|
|
return <Navigate to="/podcast-maker" replace />;
|
|
}
|
|
console.log('InitialRouteHandler: Subscription active but onboarding incomplete → Onboarding');
|
|
return <Navigate to="/onboarding" replace />;
|
|
}
|
|
|
|
// 5. Has subscription AND completed onboarding → Dashboard
|
|
console.log('InitialRouteHandler: All set (subscription + onboarding) → Dashboard');
|
|
return <Navigate to="/dashboard" replace />;
|
|
};
|
|
|
|
// Root route that chooses Landing (signed out) or InitialRouteHandler (signed in)
|
|
const RootRoute: React.FC = () => {
|
|
const { isSignedIn } = useAuth();
|
|
if (isSignedIn) {
|
|
return <InitialRouteHandler />;
|
|
}
|
|
return <Landing />;
|
|
};
|
|
|
|
// Installs Clerk auth token getter into axios clients and stores user_id
|
|
// Must render under ClerkProvider
|
|
const TokenInstaller: React.FC = () => {
|
|
const { getToken, userId, isSignedIn, signOut } = useAuth();
|
|
|
|
// Store user_id in localStorage when user signs in
|
|
useEffect(() => {
|
|
if (isSignedIn && userId) {
|
|
console.log('TokenInstaller: Storing user_id in localStorage:', userId);
|
|
localStorage.setItem('user_id', userId);
|
|
|
|
// Trigger event to notify SubscriptionContext that user is authenticated
|
|
window.dispatchEvent(new CustomEvent('user-authenticated', { detail: { userId } }));
|
|
} else if (!isSignedIn) {
|
|
// Clear user_id when signed out
|
|
console.log('TokenInstaller: Clearing user_id from localStorage');
|
|
localStorage.removeItem('user_id');
|
|
}
|
|
}, [isSignedIn, userId]);
|
|
|
|
// Install token getter for API calls
|
|
useEffect(() => {
|
|
const tokenGetter = async () => {
|
|
try {
|
|
const template = process.env.REACT_APP_CLERK_JWT_TEMPLATE;
|
|
// If a template is provided and it's not a placeholder, request a template-specific JWT
|
|
if (template && template !== 'your_jwt_template_name_here') {
|
|
// @ts-ignore Clerk types allow options object
|
|
return await getToken({ template });
|
|
}
|
|
return await getToken();
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Set token getter for main API client
|
|
setAuthTokenGetter(tokenGetter);
|
|
|
|
// Set token getter for billing API client (same function)
|
|
setBillingAuthTokenGetter(tokenGetter);
|
|
|
|
// Set token getter for media blob URL fetcher (for authenticated image/video requests)
|
|
setMediaAuthTokenGetter(tokenGetter);
|
|
}, [getToken]);
|
|
|
|
// Install Clerk signOut function for handling expired tokens
|
|
useEffect(() => {
|
|
if (signOut) {
|
|
setClerkSignOut(async () => {
|
|
await signOut();
|
|
});
|
|
}
|
|
}, [signOut]);
|
|
|
|
return null;
|
|
};
|
|
|
|
const App: React.FC = () => {
|
|
// React Hooks MUST be at the top before any conditionals
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Get CopilotKit key from localStorage or .env
|
|
const [copilotApiKey, setCopilotApiKey] = useState(() => {
|
|
const savedKey = localStorage.getItem('copilotkit_api_key');
|
|
const envKey = process.env.REACT_APP_COPILOTKIT_API_KEY || '';
|
|
const key = (savedKey || envKey).trim();
|
|
|
|
// Validate key format if present
|
|
if (key && !key.startsWith('ck_pub_')) {
|
|
console.warn('CopilotKit API key format invalid - must start with ck_pub_');
|
|
}
|
|
|
|
return key;
|
|
});
|
|
|
|
// Initialize app - loading state will be managed by InitialRouteHandler
|
|
useEffect(() => {
|
|
// Remove manual health check - connection errors are handled by ErrorBoundary
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
// Listen for CopilotKit key updates
|
|
useEffect(() => {
|
|
const handleKeyUpdate = (event: CustomEvent) => {
|
|
const newKey = event.detail?.apiKey;
|
|
if (newKey) {
|
|
console.log('App: CopilotKit key updated, reloading...');
|
|
setCopilotApiKey(newKey);
|
|
setTimeout(() => window.location.reload(), 500);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('copilotkit-key-updated', handleKeyUpdate as EventListener);
|
|
return () => window.removeEventListener('copilotkit-key-updated', handleKeyUpdate as EventListener);
|
|
}, []);
|
|
|
|
// Token installer must be inside ClerkProvider; see TokenInstaller below
|
|
|
|
if (loading) {
|
|
return (
|
|
<Box
|
|
display="flex"
|
|
flexDirection="column"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
minHeight="100vh"
|
|
gap={2}
|
|
>
|
|
<CircularProgress size={60} />
|
|
<Typography variant="h6" color="textSecondary">
|
|
Connecting to ALwrity...
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
|
|
// Get environment variables with fallbacks
|
|
const clerkPublishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY || '';
|
|
const clerkJSUrl = process.env.REACT_APP_CLERK_JS_URL;
|
|
|
|
// Show error if required keys are missing
|
|
if (!clerkPublishableKey) {
|
|
return (
|
|
<Box sx={{ p: 3, textAlign: 'center' }}>
|
|
<Typography color="error" variant="h6">
|
|
Missing Clerk Publishable Key
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
|
Please add REACT_APP_CLERK_PUBLISHABLE_KEY to your .env file
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// Render app with or without CopilotKit based on whether we have a key
|
|
const renderApp = () => {
|
|
return (
|
|
<Router>
|
|
<AuthenticatedCopilotWrapper apiKey={copilotApiKey}>
|
|
<ConditionalCopilotKit>
|
|
<TokenInstaller />
|
|
<Routes>
|
|
<Route path="/" element={<RootRoute />} />
|
|
<Route
|
|
path="/onboarding"
|
|
element={
|
|
<ErrorBoundary context="Onboarding Wizard" showDetails>
|
|
<Wizard />
|
|
</ErrorBoundary>
|
|
}
|
|
/>
|
|
{/* Error Boundary Testing - Development Only */}
|
|
{process.env.NODE_ENV === 'development' && (
|
|
<Route path="/error-test" element={<ErrorBoundaryTest />} />
|
|
)}
|
|
<Route path="/dashboard" element={<ProtectedRoute><MainDashboard /></ProtectedRoute>} />
|
|
<Route path="/seo" element={<ProtectedRoute><SEODashboard /></ProtectedRoute>} />
|
|
<Route path="/seo-dashboard" element={<ProtectedRoute><SEODashboard /></ProtectedRoute>} />
|
|
<Route path="/content-planning" element={<ProtectedRoute><ContentPlanningDashboard /></ProtectedRoute>} />
|
|
<Route path="/facebook-writer" element={<ProtectedRoute><FacebookWriter /></ProtectedRoute>} />
|
|
<Route path="/linkedin-writer" element={<ProtectedRoute><LinkedInWriter /></ProtectedRoute>} />
|
|
<Route path="/blog-writer" element={<ProtectedRoute><BlogWriter /></ProtectedRoute>} />
|
|
<Route path="/story-writer" element={<ProtectedRoute><StoryWriter /></ProtectedRoute>} />
|
|
<Route path="/story-projects" element={<ProtectedRoute><StoryProjectList /></ProtectedRoute>} />
|
|
<Route path="/youtube-creator" element={<ProtectedRoute><YouTubeCreator /></ProtectedRoute>} />
|
|
<Route path="/podcast-maker" element={<ProtectedRoute><PodcastDashboard /></ProtectedRoute>} />
|
|
<Route path="/image-studio" element={<ProtectedRoute><ImageStudioDashboard /></ProtectedRoute>} />
|
|
<Route path="/video-studio" element={<ProtectedRoute><VideoStudioDashboard /></ProtectedRoute>} />
|
|
<Route path="/video-studio/create" element={<ProtectedRoute><CreateVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/avatar" element={<ProtectedRoute><AvatarVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/enhance" element={<ProtectedRoute><EnhanceVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/extend" element={<ProtectedRoute><ExtendVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/edit" element={<ProtectedRoute><EditVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/transform" element={<ProtectedRoute><TransformVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/social" element={<ProtectedRoute><SocialVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/face-swap" element={<ProtectedRoute><FaceSwap /></ProtectedRoute>} />
|
|
<Route path="/video-studio/video-translate" element={<ProtectedRoute><VideoTranslate /></ProtectedRoute>} />
|
|
<Route path="/video-studio/video-background-remover" element={<ProtectedRoute><VideoBackgroundRemover /></ProtectedRoute>} />
|
|
<Route path="/video-studio/add-audio-to-video" element={<ProtectedRoute><AddAudioToVideo /></ProtectedRoute>} />
|
|
<Route path="/video-studio/library" element={<ProtectedRoute><LibraryVideo /></ProtectedRoute>} />
|
|
<Route path="/image-generator" element={<ProtectedRoute><CreateStudio /></ProtectedRoute>} />
|
|
<Route path="/image-editor" element={<ProtectedRoute><EditStudio /></ProtectedRoute>} />
|
|
<Route path="/image-upscale" element={<ProtectedRoute><UpscaleStudio /></ProtectedRoute>} />
|
|
<Route path="/image-control" element={<ProtectedRoute><ControlStudio /></ProtectedRoute>} />
|
|
<Route path="/image-studio/face-swap" element={<ProtectedRoute><FaceSwapStudio /></ProtectedRoute>} />
|
|
<Route path="/image-studio/compress" element={<ProtectedRoute><CompressionStudio /></ProtectedRoute>} />
|
|
<Route path="/image-studio/processing" element={<ProtectedRoute><ImageProcessingStudio /></ProtectedRoute>} />
|
|
<Route path="/image-studio/social-optimizer" element={<ProtectedRoute><SocialOptimizer /></ProtectedRoute>} />
|
|
<Route path="/asset-library" element={<ProtectedRoute><AssetLibrary /></ProtectedRoute>} />
|
|
<Route path="/campaign-creator" element={<ProtectedRoute><ProductMarketingDashboard /></ProtectedRoute>} />
|
|
<Route path="/campaign-creator/photoshoot" element={<ProtectedRoute><ProductPhotoshootStudio /></ProtectedRoute>} />
|
|
<Route path="/campaign-creator/animation" element={<ProtectedRoute><ProductAnimationStudio /></ProtectedRoute>} />
|
|
<Route path="/campaign-creator/video" element={<ProtectedRoute><ProductVideoStudio /></ProtectedRoute>} />
|
|
<Route path="/campaign-creator/avatar" element={<ProtectedRoute><ProductAvatarStudio /></ProtectedRoute>} />
|
|
<Route path="/product-marketing" element={<Navigate to="/campaign-creator" replace />} />
|
|
<Route path="/scheduler-dashboard" element={<ProtectedRoute><SchedulerDashboard /></ProtectedRoute>} />
|
|
<Route path="/billing" element={<ProtectedRoute><BillingPage /></ProtectedRoute>} />
|
|
<Route path="/approvals" element={<ProtectedRoute><ApprovalsPage /></ProtectedRoute>} />
|
|
<Route path="/team-activity" element={<ProtectedRoute><TeamActivityPage /></ProtectedRoute>} />
|
|
<Route path="/stripe-disputes" element={<ProtectedRoute><StripeDisputesDashboard /></ProtectedRoute>} />
|
|
<Route path="/pricing" element={<PricingPage />} />
|
|
<Route path="/research-test" element={<ResearchDashboard />} />
|
|
<Route path="/research-dashboard" element={<ResearchDashboard />} />
|
|
<Route path="/alwrity-researcher" element={<ResearchDashboard />} />
|
|
<Route path="/intent-research" element={<IntentResearchTest />} />
|
|
<Route path="/wix-test" element={<WixTestPage />} />
|
|
<Route path="/wix-test-direct" element={<WixTestPage />} />
|
|
<Route path="/wix/callback" element={<WixCallbackPage />} />
|
|
<Route path="/wp/callback" element={<WordPressCallbackPage />} />
|
|
<Route path="/gsc/callback" element={<GSCAuthCallback />} />
|
|
<Route path="/bing/callback" element={<BingCallbackPage />} />
|
|
<Route path="/bing-analytics-storage" element={<ProtectedRoute><BingAnalyticsStorage /></ProtectedRoute>} />
|
|
</Routes>
|
|
</ConditionalCopilotKit>
|
|
</AuthenticatedCopilotWrapper>
|
|
</Router>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ErrorBoundary
|
|
context="Application Root"
|
|
showDetails={process.env.NODE_ENV === 'development'}
|
|
onError={(error, errorInfo) => {
|
|
// Custom error handler - send to analytics/monitoring
|
|
console.error('Global error caught:', { error, errorInfo });
|
|
// TODO: Send to error tracking service (Sentry, LogRocket, etc.)
|
|
}}
|
|
>
|
|
<ClerkProvider publishableKey={clerkPublishableKey} clerkJSUrl={clerkJSUrl}>
|
|
<SubscriptionProvider>
|
|
<OnboardingProvider>
|
|
{renderApp()}
|
|
</OnboardingProvider>
|
|
</SubscriptionProvider>
|
|
</ClerkProvider>
|
|
</ErrorBoundary>
|
|
);
|
|
};
|
|
|
|
export default App;
|