fix: add missing App components for Vercel deployment

This commit is contained in:
ajaysi
2026-04-03 18:32:22 +05:30
parent 027638dfb9
commit a8c80c5b75
3 changed files with 403 additions and 0 deletions

View File

@@ -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<ConditionalCopilotKitProps> = ({ children }) => {
return <>{children}</>;
};
interface AuthenticatedCopilotWrapperProps {
children: React.ReactNode;
apiKey: string;
}
export const AuthenticatedCopilotWrapper: React.FC<AuthenticatedCopilotWrapperProps> = ({ 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 (
<CopilotKitHealthProvider initialHealthStatus={true}>
<CopilotKitDegradedBanner />
<ErrorBoundary
context="CopilotKit"
showDetails={process.env.NODE_ENV === 'development'}
fallback={
<div style={{ padding: 24, textAlign: 'center' }}>
<h6 style={{ color: '#ed6c02', marginBottom: 8 }}>Chat Unavailable</h6>
<p style={{ color: '#9e9e9e', fontSize: 14 }}>
CopilotKit encountered an error. The app continues to work with manual controls.
</p>
</div>
}
>
<CopilotKit
publicApiKey={apiKey}
showDevConsole={false}
onError={handleCopilotKitError}
>
{children}
</CopilotKit>
</ErrorBoundary>
</CopilotKitHealthProvider>
);
}
return (
<CopilotKitHealthProvider initialHealthStatus={false}>
<CopilotKitDegradedBanner />
{children}
</CopilotKitHealthProvider>
);
};

View File

@@ -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 <Navigate to="/podcast-maker" replace />;
}
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 (
<ConnectionErrorPage
onRetry={handleRetry}
onGoHome={handleGoHome}
message={connectionError.error?.message || "Backend service is not available. Please check if the server is running."}
title="Connection Error"
/>
);
}
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>
);
}
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>
);
}
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>
);
}
if (!subscription) {
if (isOnboardingComplete) {
console.log('InitialRouteHandler: Onboarding complete but no subscription data → Dashboard (allow access)');
return <Navigate to="/dashboard" replace />;
}
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>
);
}
if (!subscription) {
if (isOnboardingComplete) {
console.log('InitialRouteHandler: Onboarding complete but no subscription data → Dashboard (allow access)');
return <Navigate to="/dashboard" replace />;
}
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>
);
}
if (shouldSkipOnboarding()) {
console.log('InitialRouteHandler: Demo mode - no subscription but allowing access to podcast-maker');
return <Navigate to="/podcast-maker" replace />;
}
console.log('InitialRouteHandler: No subscription data after check → Pricing page');
return <Navigate to="/pricing" replace />;
}
}
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 <Navigate to="/pricing" replace />;
}
console.log('InitialRouteHandler: Inactive subscription - allowing access to show modal');
}
if (!isOnboardingComplete) {
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 />;
}
console.log('InitialRouteHandler: All set (subscription + onboarding) → Dashboard');
return <Navigate to="/dashboard" replace />;
};
export default InitialRouteHandler;

View File

@@ -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;