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