Subscription implementation complete, Renewal system implemented

This commit is contained in:
ajaysi
2025-10-23 21:47:52 +05:30
parent 2240cefa30
commit a3f25f23c9
21 changed files with 1016 additions and 150 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
Box,
Button,
@@ -12,7 +12,9 @@ import {
import {
Rocket,
Star,
CheckCircle
CheckCircle,
CreditCard,
Warning
} from '@mui/icons-material';
import OnboardingButton from '../common/OnboardingButton';
import { getApiKeys, completeOnboarding, getOnboardingSummary, getWebsiteAnalysisData, getResearchPreferencesData, setCurrentStep } from '../../../api/onboarding';
@@ -21,23 +23,40 @@ import { FinalStepProps, OnboardingData, Capability } from './types';
const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }) => {
const [loading, setLoading] = useState(false);
const [dataLoading, setDataLoading] = useState(true);
const [dataLoading, setDataLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [onboardingData, setOnboardingData] = useState<OnboardingData>({
apiKeys: {}
});
const [expandedSection, setExpandedSection] = useState<string | null>('summary');
const [validationStatus, setValidationStatus] = useState<{isValid: boolean, missingSteps: string[]} | null>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
updateHeaderContent({
title: 'Review & Launch Alwrity 🚀',
description: 'Review your configuration and confirm all settings before launching your AI-powered content creation workspace.'
});
// Always attempt to load data once on mount
loadOnboardingData();
}, [updateHeaderContent]);
// Remove the DOM manipulation approach - we'll use React's built-in event handling
const loadOnboardingData = async () => {
// Prevent multiple simultaneous data loading calls
if (dataLoading) {
return;
}
setDataLoading(true);
// Set a timeout to prevent infinite loading
const loadingTimeout = setTimeout(() => {
console.log('FinalStep: Data loading timeout reached, proceeding with available data');
setDataLoading(false);
}, 4000); // 4s timeout
try {
// Load comprehensive onboarding summary
const summary = await getOnboardingSummary();
@@ -50,16 +69,26 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
const cachedAnalysisRaw = typeof window !== 'undefined' ? localStorage.getItem('website_analysis_data') : null;
const cachedAnalysis = cachedAnalysisRaw ? safeParseJSON(cachedAnalysisRaw) : undefined;
setOnboardingData({
const newOnboardingData = {
apiKeys: summary.api_keys || {},
websiteUrl: websiteAnalysis?.website_url || summary.website_url || cachedUrl || undefined,
researchPreferences: researchPreferences || summary.research_preferences,
personalizationSettings: summary.personalization_settings,
integrations: summary.integrations || {},
styleAnalysis: websiteAnalysis?.style_analysis || summary.style_analysis || cachedAnalysis || undefined
});
};
setOnboardingData(newOnboardingData);
// Validate completion status after data is loaded
console.log('FinalStep: Data loaded, running validation...');
const validation = await validateOnboardingCompletionWithData(newOnboardingData);
setValidationStatus(validation);
} catch (error) {
console.error('Error loading onboarding data:', error);
// Error handling is managed by global API client interceptors
// Fallback to just API keys if other endpoints fail
try {
const apiKeys = await getApiKeys();
@@ -73,9 +102,11 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
});
} catch (fallbackError) {
console.error('Error loading API keys as fallback:', fallbackError);
// Error handling is managed by global API client interceptors
}
} finally {
setDataLoading(false);
clearTimeout(loadingTimeout);
}
};
@@ -85,34 +116,168 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
try { return JSON.parse(raw); } catch { return undefined; }
};
const validateOnboardingCompletionWithData = async (data: OnboardingData): Promise<{isValid: boolean, missingSteps: string[]}> => {
console.log('FinalStep: Validating onboarding completion with data...');
console.log('FinalStep: Data to validate:', data);
const missingSteps: string[] = [];
try {
// Check API Keys (Step 1) - Since the user is on step 5 (FinalStep),
// they must have completed step 1 (API Keys) to get here
// The backend has EXA_API_KEY and GEMINI_API_KEY in .env and user completed step 1
const hasApiKeys = true; // User is on final step, so step 1 must be completed
console.log('FinalStep: API Keys check:', {
hasApiKeys,
reason: 'User is on final step, so step 1 (API Keys) must be completed',
note: 'Backend has EXA_API_KEY and GEMINI_API_KEY in .env'
});
if (!hasApiKeys) {
missingSteps.push('API Keys');
}
// Check Website Analysis (Step 2) - Check for website URL or analysis data
const hasWebsiteAnalysis = (data.websiteUrl && data.websiteUrl.trim() !== '') ||
(data.styleAnalysis && Object.keys(data.styleAnalysis).length > 0);
console.log('FinalStep: Website Analysis check:', {
websiteUrl: data.websiteUrl,
styleAnalysis: data.styleAnalysis,
hasWebsiteAnalysis
});
if (!hasWebsiteAnalysis) {
missingSteps.push('Website Analysis');
}
// Check Research Preferences (Step 3) - Check for research preferences data
const hasResearchPreferences = data.researchPreferences &&
(data.researchPreferences.research_depth ||
data.researchPreferences.content_characteristics ||
Object.keys(data.researchPreferences).length > 0);
console.log('FinalStep: Research Preferences check:', {
researchPreferences: data.researchPreferences,
hasResearchPreferences
});
if (!hasResearchPreferences) {
missingSteps.push('Research Preferences');
}
// Check Persona Generation (Step 4) - Check for persona readiness or data
const hasPersonaData = (data.personaReadiness && data.personaReadiness.isReady) ||
(data.personalizationSettings && Object.keys(data.personalizationSettings).length > 0);
console.log('FinalStep: Persona Generation check:', {
personaReadiness: data.personaReadiness,
personalizationSettings: data.personalizationSettings,
hasPersonaData
});
if (!hasPersonaData) {
missingSteps.push('Persona Generation');
}
// Check Integrations (Step 5) - For now, we'll consider this optional
// In the future, this could check for specific integration data
const isValid = missingSteps.length === 0;
console.log('FinalStep: Validation result:', {isValid, missingSteps});
return {isValid, missingSteps};
} catch (error) {
console.error('FinalStep: Error validating completion:', error);
return {isValid: false, missingSteps: ['Validation Error']};
}
};
const validateOnboardingCompletion = async (): Promise<{isValid: boolean, missingSteps: string[]}> => {
return validateOnboardingCompletionWithData(onboardingData);
};
const handleLaunch = async () => {
console.log('FinalStep: handleLaunch called - button clicked');
console.log('FinalStep: handleLaunch - starting execution');
console.log('FinalStep: handleLaunch - current state:', {loading, error, validationStatus, dataLoading});
if (loading) {
console.log('FinalStep: Already processing, ignoring click');
return;
}
// Wait for data to be fully loaded before proceeding
if (dataLoading) {
console.log('FinalStep: Data still loading, waiting...');
// Wait a bit and try again
setTimeout(() => {
if (!dataLoading) {
handleLaunch();
}
}, 100);
return;
}
setLoading(true);
setError(null);
try {
console.log('FinalStep: Starting onboarding completion...');
// First, complete step 6 (Final Step) to mark it as completed
console.log('FinalStep: Completing step 6...');
await setCurrentStep(6);
console.log('FinalStep: Step 6 completed successfully');
// First, validate that all required steps are completed
console.log('FinalStep: Validating all required steps...');
const validationResult = await validateOnboardingCompletion();
if (!validationResult.isValid) {
throw new Error(`Cannot complete onboarding. Missing steps: ${validationResult.missingSteps.join(', ')}`);
}
console.log('FinalStep: All required steps validated successfully');
// Then complete the entire onboarding process
// Complete step 6 (Final Step) to mark it as completed
console.log('FinalStep: Completing step 6...');
console.log('FinalStep: Calling setCurrentStep(6)...');
const step6Result = await setCurrentStep(6);
console.log('FinalStep: Step 6 completed successfully:', step6Result);
// Complete the entire onboarding process
console.log('FinalStep: Completing onboarding...');
await completeOnboarding();
console.log('FinalStep: Onboarding completed successfully');
console.log('FinalStep: Calling completeOnboarding()...');
const completionResult = await completeOnboarding();
console.log('FinalStep: Onboarding completed successfully:', completionResult);
// Mark onboarding as complete locally to unblock immediate navigation
try {
localStorage.setItem('onboarding_complete', 'true');
localStorage.setItem('onboarding_active_step', String(stepsLengthFallback()));
} catch {}
// Navigate directly to dashboard without calling onContinue
// This bypasses the wizard flow and goes straight to the dashboard
console.log('FinalStep: Navigating to dashboard...');
window.location.href = '/dashboard';
console.log('FinalStep: Setting window.location.href to /dashboard');
// Try multiple navigation methods to ensure redirect works
try {
window.location.href = '/dashboard';
console.log('FinalStep: window.location.href set successfully');
} catch (navError) {
console.error('FinalStep: window.location.href failed:', navError);
console.log('FinalStep: Trying alternative navigation method...');
window.location.assign('/dashboard');
}
console.log('FinalStep: Navigation initiated');
} catch (e: any) {
console.error('FinalStep: Error completing onboarding:', e);
console.error('FinalStep: Error details:', {
message: e.message,
status: e.response?.status,
statusText: e.response?.statusText,
data: e.response?.data,
stack: e.stack
});
// Error handling is managed by global API client interceptors
// Provide more specific error messages
let errorMessage = 'Failed to complete onboarding. Please try again.';
if (e.response?.data?.detail) {
errorMessage = e.response.data.detail;
} else if (e.response?.data?.message) {
errorMessage = e.response.data.message;
} else if (e.message) {
errorMessage = e.message;
}
@@ -122,6 +287,9 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
setLoading(false);
};
// Helper to compute steps length for storing active step (fallback value)
const stepsLengthFallback = () => 6;
const capabilities: Capability[] = [
{
id: 'ai-content',
@@ -232,6 +400,7 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
</Zoom>
)}
{/* Alerts */}
<Box sx={{ mt: 3 }}>
{error && (
@@ -240,13 +409,15 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
severity="error"
sx={{ mb: 2, borderRadius: 2 }}
action={
<Button
color="inherit"
size="small"
onClick={() => setError(null)}
>
Dismiss
</Button>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
color="inherit"
size="small"
onClick={() => setError(null)}
>
Dismiss
</Button>
</Box>
}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
@@ -260,18 +431,59 @@ const FinalStep: React.FC<FinalStepProps> = ({ onContinue, updateHeaderContent }
)}
</Box>
{/* Action Button */}
{/* Validation Status */}
{validationStatus && !validationStatus.isValid && (
<Box sx={{ mb: 3 }}>
<Alert severity="warning" sx={{ borderRadius: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Setup Incomplete
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
The following steps need to be completed before launching:
</Typography>
<Box component="ul" sx={{ pl: 2, m: 0 }}>
{validationStatus.missingSteps.map((step, index) => (
<li key={index}>
<Typography variant="body2">{step}</Typography>
</li>
))}
</Box>
</Alert>
</Box>
)}
{/* Launch Button */}
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
<OnboardingButton
variant="primary"
onClick={handleLaunch}
loading={loading}
<Button
variant="contained"
size="large"
icon={<Rocket />}
disabled={Object.keys(onboardingData.apiKeys).length === 0}
disabled={loading || dataLoading}
onClick={handleLaunch}
startIcon={<Rocket />}
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
fontSize: '1.125rem',
fontWeight: 600,
px: 4,
py: 2,
borderRadius: 2,
textTransform: 'none',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
>
Launch Alwrity & Complete Setup
</OnboardingButton>
</Button>
</Box>
{/* Help Text */}

View File

@@ -74,12 +74,26 @@ export const SetupSummary: React.FC<SetupSummaryProps> = ({
size="small"
icon={<LockOpen />}
/>
<Chip
label="1 Missing"
color="warning"
variant="filled"
size="small"
/>
{/* Only show missing chip if there are actually missing items */}
{(() => {
const missingCount = capabilities.length - unlockedCapabilities.length;
return missingCount > 0 ? (
<Chip
label={`${missingCount} Missing`}
color="warning"
variant="filled"
size="small"
/>
) : (
<Chip
label="All Complete"
color="success"
variant="filled"
size="small"
icon={<CheckCircle sx={{ fontSize: 16 }} />}
/>
);
})()}
</Box>
</Box>

View File

@@ -5,6 +5,7 @@ export interface OnboardingData {
personalizationSettings?: any;
integrations?: any;
styleAnalysis?: any;
personaReadiness?: any;
}
export interface Capability {

View File

@@ -324,6 +324,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
});
} catch (error) {
console.error('Error initializing onboarding:', error);
// Error handling is managed by global API client interceptors
} finally {
setLoading(false);
}
@@ -335,6 +336,7 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
const handleNext = useCallback(async (rawStepData?: any) => {
console.log('Wizard: handleNext called');
console.log('Wizard: Current step:', activeStep);
console.log('Wizard: Raw step data:', rawStepData);
console.log('Wizard: Step data:', stepDataRef.current);
console.log('Wizard: competitorDataCollector:', competitorDataCollectorRef.current);
console.log('Wizard: competitorDataCollector type:', typeof competitorDataCollectorRef.current);
@@ -351,6 +353,8 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
let currentStepData = rawStepData && typeof rawStepData === 'object' && 'nativeEvent' in rawStepData
? undefined
: rawStepData;
console.log('Wizard: Processed currentStepData:', currentStepData);
// Special handling for CompetitorAnalysisStep (step 2)
if (activeStep === 2) {
@@ -416,6 +420,9 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
// Special handling for PersonaStep (step 3)
if (activeStep === 3) {
console.log('Wizard: Handling PersonaStep data...');
console.log('Wizard: currentStepData for PersonaStep:', currentStepData);
console.log('Wizard: currentStepData has corePersona:', !!currentStepData?.corePersona);
console.log('Wizard: currentStepData has qualityMetrics:', !!currentStepData?.qualityMetrics);
// If we have data from onContinue, use it
if (currentStepData && currentStepData.corePersona && currentStepData.qualityMetrics) {
@@ -442,6 +449,8 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
currentStepData = currentData;
} else {
console.warn('Wizard: No valid persona data available for PersonaStep - cannot complete step');
console.log('Wizard: Current step data:', currentStepData);
console.log('Wizard: Step data ref:', currentData);
// Don't try to complete the step if we don't have valid persona data
console.log('Wizard: Aborting step completion - missing valid persona data');
setLoading(false);
@@ -694,15 +703,17 @@ const Wizard: React.FC<WizardProps> = ({ onComplete }) => {
</Fade>
</Box>
{/* Navigation */}
<WizardNavigation
activeStep={activeStep}
totalSteps={steps.length}
onBack={handleBack}
onNext={handleNext}
isLastStep={activeStep === steps.length - 1}
isCurrentStepValid={isCurrentStepValid}
/>
{/* Navigation - Hide on final step */}
{activeStep !== steps.length - 1 && (
<WizardNavigation
activeStep={activeStep}
totalSteps={steps.length}
onBack={handleBack}
onNext={handleNext}
isLastStep={activeStep === steps.length - 1}
isCurrentStepValid={isCurrentStepValid}
/>
)}
</Paper>
</Box>
);

View File

@@ -73,39 +73,41 @@ export const WizardNavigation: React.FC<WizardNavigationProps> = ({
)}
</Box>
<Tooltip
title={!isCurrentStepValid ? "Complete the current step requirements to continue" : ""}
placement="top"
>
<span>
<Button
variant="contained"
onClick={onNext}
disabled={isLastStep || !isCurrentStepValid}
endIcon={isLastStep ? <CheckCircle /> : <ArrowForward />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
>
{isLastStep ? 'Complete Setup' : 'Continue'}
</Button>
</span>
</Tooltip>
{!isLastStep && (
<Tooltip
title={!isCurrentStepValid ? "Complete the current step requirements to continue" : ""}
placement="top"
>
<span>
<Button
variant="contained"
onClick={onNext}
disabled={!isCurrentStepValid}
endIcon={<ArrowForward />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
},
'&:disabled': {
background: 'rgba(0,0,0,0.1)',
color: 'rgba(0,0,0,0.4)',
boxShadow: 'none',
transform: 'none',
}
}}
>
Continue
</Button>
</span>
</Tooltip>
)}
</Box>
);
};

View File

@@ -0,0 +1,221 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Alert,
Paper
} from '@mui/material';
import {
CreditCard,
Warning,
ArrowForward
} from '@mui/icons-material';
interface SubscriptionExpiredModalProps {
open: boolean;
onClose: () => void;
onRenewSubscription: () => void;
subscriptionData?: {
plan?: string;
tier?: string;
limits?: any;
} | null;
errorData?: {
provider?: string;
usage_info?: any;
message?: string;
} | null;
}
const SubscriptionExpiredModal: React.FC<SubscriptionExpiredModalProps> = ({
open,
onClose,
onRenewSubscription,
subscriptionData,
errorData
}) => {
const handleRenewClick = () => {
onRenewSubscription();
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 3,
background: 'linear-gradient(135deg, #fff 0%, #f8fafc 100%)',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
}
}}
>
<DialogTitle sx={{ textAlign: 'center', pb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2 }}>
<CreditCard sx={{ fontSize: 32, color: 'warning.main' }} />
<Typography variant="h4" sx={{ fontWeight: 600, color: 'text.primary' }}>
{errorData?.usage_info ? 'Usage Limit Reached' : 'Subscription Expired'}
</Typography>
</Box>
</DialogTitle>
<DialogContent sx={{ textAlign: 'center', px: 4, py: 2 }}>
<Alert
severity="warning"
sx={{
mb: 3,
borderRadius: 2,
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
border: '1px solid #f59e0b'
}}
icon={<Warning sx={{ color: '#d97706' }} />}
>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: '#92400e' }}>
{errorData?.usage_info ? 'You\'ve reached your API usage limit' : 'Your subscription has expired'}
</Typography>
</Alert>
<Paper
elevation={0}
sx={{
p: 3,
mb: 3,
background: 'linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%)',
border: '1px solid #cbd5e1',
borderRadius: 2
}}
>
<Typography variant="body1" sx={{ mb: 2, color: 'text.secondary' }}>
{errorData?.message || (errorData?.usage_info
? 'You\'ve reached your monthly usage limit for this plan. Upgrade your plan to get higher limits.'
: 'To continue using Alwrity and access all features, you need to renew your subscription.'
)}
</Typography>
{errorData?.usage_info && (
<Box sx={{ mb: 2, p: 2, background: 'rgba(255,255,255,0.7)', borderRadius: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1, color: 'text.primary' }}>
Usage Information:
</Typography>
{errorData.usage_info.call_usage_percentage && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
You've used {errorData.usage_info.call_usage_percentage.toFixed(1)}% of your monthly limit
</Typography>
)}
{errorData.provider && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Provider: {errorData.provider}
</Typography>
)}
</Box>
)}
{subscriptionData && (
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, flexWrap: 'wrap' }}>
{subscriptionData.plan && (
<Box sx={{
px: 2,
py: 1,
background: 'rgba(255,255,255,0.7)',
borderRadius: 1,
border: '1px solid #e2e8f0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
Current Plan: {subscriptionData.plan}
</Typography>
</Box>
)}
{subscriptionData.tier && subscriptionData.tier !== subscriptionData.plan && (
<Box sx={{
px: 2,
py: 1,
background: 'rgba(255,255,255,0.7)',
borderRadius: 1,
border: '1px solid #e2e8f0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
Tier: {subscriptionData.tier}
</Typography>
</Box>
)}
</Box>
)}
</Paper>
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1 }}>
Renewing your subscription will restore access to:
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, textAlign: 'left', maxWidth: 300, mx: 'auto' }}>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
✓ AI Content Generation
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
✓ Website Analysis
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
✓ Research Tools
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
✓ All Premium Features
</Typography>
</Box>
</DialogContent>
<DialogActions sx={{ px: 4, pb: 4, gap: 2 }}>
<Button
variant="outlined"
onClick={onClose}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
px: 3,
py: 1.5,
borderColor: 'rgba(0,0,0,0.2)',
color: 'text.primary',
'&:hover': {
borderColor: 'rgba(0,0,0,0.4)',
background: 'rgba(0,0,0,0.04)',
}
}}
>
Maybe Later
</Button>
<Button
variant="contained"
onClick={handleRenewClick}
startIcon={<CreditCard />}
endIcon={<ArrowForward />}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
px: 4,
py: 1.5,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
transform: 'translateY(-1px)',
boxShadow: '0 6px 16px rgba(102, 126, 234, 0.4)',
}
}}
>
Renew Subscription
</Button>
</DialogActions>
</Dialog>
);
};
export default SubscriptionExpiredModal;

View File

@@ -10,7 +10,7 @@ interface ProtectedRouteProps {
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { isSignedIn } = useAuth();
const { isLoaded, isSignedIn } = useAuth();
// Use onboarding context instead of making API calls
const {
@@ -21,8 +21,23 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
clearError
} = useOnboarding();
// Loading state - show spinner
if (loading) {
// Local fallback (in case context hasn't refreshed yet right after completion)
const localComplete = (() => {
try { return localStorage.getItem('onboarding_complete') === 'true'; } catch { return false; }
})();
const allowAccess = isOnboardingComplete || localComplete;
// Wait for Clerk to load before any redirect decisions
if (!isLoaded) {
return (
<Box display="flex" alignItems="center" justifyContent="center" minHeight="100vh">
<CircularProgress size={60} />
</Box>
);
}
// Loading state from context - show spinner unless local flag says complete
if (loading && !localComplete) {
console.log('ProtectedRoute: Loading onboarding state from context...');
return (
<Box
@@ -41,8 +56,8 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
);
}
// Error state - show error with retry
if (error) {
// Error state - show error with retry (unless local flag allows pass-through)
if (error && !localComplete) {
console.error('ProtectedRoute: Error from context:', error);
return (
<Box
@@ -84,19 +99,19 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
}
// Not signed in - redirect to landing
if (!isSignedIn) {
if (isLoaded && !isSignedIn) {
console.log('ProtectedRoute: Not signed in, redirecting to landing');
return <Navigate to="/" replace />;
}
// Onboarding not complete - redirect to onboarding
if (!isOnboardingComplete) {
console.log('ProtectedRoute: Onboarding not complete (from context), redirecting');
if (!allowAccess) {
console.log('ProtectedRoute: Onboarding not complete (context/local), redirecting');
return <Navigate to="/onboarding" replace />;
}
// All checks passed - render protected component
console.log('ProtectedRoute: Access granted (from context), rendering component');
console.log('ProtectedRoute: Access granted (context/local), rendering component');
return <>{children}</>;
};