From a8c80c5b75aefbf1fd1c5dfdf7e04dfa4ee8fac5 Mon Sep 17 00:00:00 2001 From: ajaysi Date: Fri, 3 Apr 2026 18:32:22 +0530 Subject: [PATCH] fix: add missing App components for Vercel deployment --- .../src/components/App/CopilotWrappers.tsx | 91 ++++++ .../components/App/InitialRouteHandler.tsx | 261 ++++++++++++++++++ .../src/components/App/TokenInstaller.tsx | 51 ++++ 3 files changed, 403 insertions(+) create mode 100644 frontend/src/components/App/CopilotWrappers.tsx create mode 100644 frontend/src/components/App/InitialRouteHandler.tsx create mode 100644 frontend/src/components/App/TokenInstaller.tsx diff --git a/frontend/src/components/App/CopilotWrappers.tsx b/frontend/src/components/App/CopilotWrappers.tsx new file mode 100644 index 00000000..d985d621 --- /dev/null +++ b/frontend/src/components/App/CopilotWrappers.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { useAuth } from '@clerk/clerk-react'; +import { useLocation } from 'react-router-dom'; +import { CopilotKit } from "@copilotkit/react-core"; +import { CopilotKitHealthProvider } from '../../contexts/CopilotKitHealthContext'; +import CopilotKitDegradedBanner from '../shared/CopilotKitDegradedBanner'; +import ErrorBoundary from '../shared/ErrorBoundary'; + +interface ConditionalCopilotKitProps { + children: React.ReactNode; +} + +export const ConditionalCopilotKit: React.FC = ({ children }) => { + return <>{children}; +}; + +interface AuthenticatedCopilotWrapperProps { + children: React.ReactNode; + apiKey: string; +} + +export const AuthenticatedCopilotWrapper: React.FC = ({ children, apiKey }) => { + const { isSignedIn } = useAuth(); + const location = useLocation(); + + const shouldExcludeCopilot = !isSignedIn || location.pathname.startsWith('/onboarding'); + + if (shouldExcludeCopilot) { + return <>{children}; + } + + const hasKey = apiKey && apiKey.trim(); + + if (hasKey) { + const handleCopilotKitError = (e: any) => { + console.error("CopilotKit Error:", e); + + const errorMessage = e?.error?.message || e?.message || 'CopilotKit error occurred'; + const errorType = errorMessage.toLowerCase(); + + const isFatalError = + errorType.includes('cors') || + errorType.includes('ssl') || + errorType.includes('certificate') || + errorType.includes('403') || + errorType.includes('forbidden') || + errorType.includes('ERR_CERT_COMMON_NAME_INVALID'); + + window.dispatchEvent(new CustomEvent('copilotkit-error', { + detail: { + error: e, + errorMessage, + isFatal: isFatalError, + } + })); + }; + + return ( + + + +
Chat Unavailable
+

+ CopilotKit encountered an error. The app continues to work with manual controls. +

+ + } + > + + {children} + +
+
+ ); + } + + return ( + + + {children} + + ); +}; diff --git a/frontend/src/components/App/InitialRouteHandler.tsx b/frontend/src/components/App/InitialRouteHandler.tsx new file mode 100644 index 00000000..33bf3324 --- /dev/null +++ b/frontend/src/components/App/InitialRouteHandler.tsx @@ -0,0 +1,261 @@ +import React, { useState, useEffect } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { Box, CircularProgress, Typography } from '@mui/material'; +import { useOnboarding } from '../../contexts/OnboardingContext'; +import { useSubscription } from '../../contexts/SubscriptionContext'; +import { useOAuthTokenAlerts } from '../../hooks/useOAuthTokenAlerts'; +import { shouldSkipOnboarding } from '../../utils/demoMode'; +import ConnectionErrorPage from '../shared/ConnectionErrorPage'; + +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, + }); + + useOAuthTokenAlerts({ + enabled: subscription?.active === true, + interval: 60000, + }); + + useEffect(() => { + const timeoutId = setTimeout(async () => { + const maxRetries = 3; + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + await checkSubscription(); + break; + } catch (err) { + console.error(`App: Subscription check attempt ${attempt + 1} failed:`, err); + + const isConnectionError = err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError'); + + if (isConnectionError && attempt < maxRetries - 1) { + const delay = 1000 * Math.pow(2, attempt); + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + + if (attempt === maxRetries - 1 || !isConnectionError) { + if (isConnectionError) { + setConnectionError({ + hasError: true, + error: err as Error, + }); + } + } + } + } + }, 100); + + return () => clearTimeout(timeoutId); + }, []); + + const urlParams = new URLSearchParams(location.search); + const isCheckoutSuccess = urlParams.get('subscription') === 'success'; + + useEffect(() => { + if (subscription && !subscriptionLoading) { + 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...'); + + if (!isCheckoutSuccess) { + initializeOnboarding(); + } + } + } + }, [subscription, subscriptionLoading, initializeOnboarding, isCheckoutSuccess]); + + if (isCheckoutSuccess && subscription?.active && shouldSkipOnboarding()) { + console.log('InitialRouteHandler: Early redirect - Stripe checkout success in demo mode → Podcast Maker'); + return ; + } + + if (connectionError.hasError) { + const handleRetry = () => { + setConnectionError({ + hasError: false, + error: null, + }); + checkSubscription().catch((err) => { + if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) { + setConnectionError({ + hasError: true, + error: err, + }); + } + }); + }; + + const handleGoHome = () => { + window.location.href = '/'; + }; + + return ( + + ); + } + + const isDemoMode = shouldSkipOnboarding(); + const isActiveSubscriber = Boolean(subscription && subscription.active && subscription.plan !== 'none'); + const waitingForOnboardingInit = !isDemoMode && isActiveSubscriber && (loading || !data); + if (waitingForOnboardingInit) { + return ( + + + + Preparing your workspace... + + + ); + } + + if (error) { + return ( + + + Error + + + {error} + + + ); + } + + if (subscriptionLoading) { + return ( + + + + Checking subscription... + + + ); + } + + if (!subscription) { + if (isOnboardingComplete) { + console.log('InitialRouteHandler: Onboarding complete but no subscription data → Dashboard (allow access)'); + return ; + } + + if (subscriptionLoading) { + return ( + + + + Checking subscription... + + + ); + } + + if (!subscription) { + if (isOnboardingComplete) { + console.log('InitialRouteHandler: Onboarding complete but no subscription data → Dashboard (allow access)'); + return ; + } + + if (subscriptionLoading) { + return ( + + + + Checking subscription... + + + ); + } + + if (shouldSkipOnboarding()) { + console.log('InitialRouteHandler: Demo mode - no subscription but allowing access to podcast-maker'); + return ; + } + + console.log('InitialRouteHandler: No subscription data after check → Pricing page'); + return ; + } + } + + const isNewUser = !subscription || subscription.plan === 'none'; + + if (isNewUser || !subscription.active) { + console.log('InitialRouteHandler: No active subscription - modal will be shown by SubscriptionContext'); + if (isNewUser) { + console.log('InitialRouteHandler: New user (no subscription) → Pricing page'); + return ; + } + console.log('InitialRouteHandler: Inactive subscription - allowing access to show modal'); + } + + if (!isOnboardingComplete) { + if (shouldSkipOnboarding()) { + console.log('InitialRouteHandler: Demo mode - skipping onboarding → Podcast Maker'); + return ; + } + console.log('InitialRouteHandler: Subscription active but onboarding incomplete → Onboarding'); + return ; + } + + console.log('InitialRouteHandler: All set (subscription + onboarding) → Dashboard'); + return ; +}; + +export default InitialRouteHandler; diff --git a/frontend/src/components/App/TokenInstaller.tsx b/frontend/src/components/App/TokenInstaller.tsx new file mode 100644 index 00000000..5ce65c58 --- /dev/null +++ b/frontend/src/components/App/TokenInstaller.tsx @@ -0,0 +1,51 @@ +import { useEffect } from 'react'; +import { useAuth } from '@clerk/clerk-react'; +import { setAuthTokenGetter, setClerkSignOut } from '../../api/client'; +import { setMediaAuthTokenGetter } from '../../utils/fetchMediaBlobUrl'; +import { setBillingAuthTokenGetter } from '../../services/billingService'; + +const TokenInstaller: React.FC = () => { + const { getToken, userId, isSignedIn, signOut } = useAuth(); + + useEffect(() => { + if (isSignedIn && userId) { + console.log('TokenInstaller: Storing user_id in localStorage:', userId); + localStorage.setItem('user_id', userId); + + window.dispatchEvent(new CustomEvent('user-authenticated', { detail: { userId } })); + } else if (!isSignedIn) { + console.log('TokenInstaller: Clearing user_id from localStorage'); + localStorage.removeItem('user_id'); + } + }, [isSignedIn, userId]); + + useEffect(() => { + const tokenGetter = async () => { + try { + const template = process.env.REACT_APP_CLERK_JWT_TEMPLATE; + if (template && template !== 'your_jwt_template_name_here') { + return await getToken({ template }); + } + return await getToken(); + } catch { + return null; + } + }; + + setAuthTokenGetter(tokenGetter); + setBillingAuthTokenGetter(tokenGetter); + setMediaAuthTokenGetter(tokenGetter); + }, [getToken]); + + useEffect(() => { + if (signOut) { + setClerkSignOut(async () => { + await signOut(); + }); + } + }, [signOut]); + + return null; +}; + +export default TokenInstaller;