fix: add missing App components for Vercel deployment
This commit is contained in:
91
frontend/src/components/App/CopilotWrappers.tsx
Normal file
91
frontend/src/components/App/CopilotWrappers.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
261
frontend/src/components/App/InitialRouteHandler.tsx
Normal file
261
frontend/src/components/App/InitialRouteHandler.tsx
Normal 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;
|
||||
51
frontend/src/components/App/TokenInstaller.tsx
Normal file
51
frontend/src/components/App/TokenInstaller.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user