Files
ALwrity/frontend/src/contexts/OnboardingContext.tsx
ajaysi 43c6ceab2f fix: Skip onboarding calls in podcast-only demo mode
- Add demoMode utility for consistent demo mode detection
- Skip onboarding API calls in OnboardingContext when in demo mode
- Redirect to /podcast-maker instead of /onboarding in demo mode
2026-03-30 09:38:48 +05:30

320 lines
9.1 KiB
TypeScript

import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
import { useAuth } from '@clerk/clerk-react';
import { apiClient } from '../api/client';
import { shouldSkipOnboarding } from '../utils/demoMode';
/**
* Onboarding Context
*
* Provides centralized onboarding state management across the application.
* Eliminates redundant API calls by sharing state between components.
*
* Features:
* - Single API call on initialization
* - Cached state shared across components
* - Manual refresh capability
* - Automatic state synchronization
* - Loading and error states
*/
export interface OnboardingUser {
id: string;
email: string;
first_name: string;
last_name: string;
clerk_user_id: string;
}
export interface OnboardingStep {
step_number: number;
title: string;
description: string;
status: 'pending' | 'in_progress' | 'completed' | 'skipped';
completed_at: string | null;
has_data: boolean;
}
export interface OnboardingStatus {
is_completed: boolean;
current_step: number;
completion_percentage: number;
next_step: number | null;
started_at: string;
last_updated: string;
completed_at: string | null;
can_proceed_to_final: boolean;
steps: OnboardingStep[];
}
export interface OnboardingSession {
session_id: string;
initialized_at: string;
}
export interface OnboardingData {
user: OnboardingUser | null;
onboarding: OnboardingStatus | null;
session: OnboardingSession | null;
}
interface OnboardingContextValue {
// State
data: OnboardingData | null;
loading: boolean;
error: string | null;
// Computed properties
isOnboardingComplete: boolean;
currentStep: number;
completionPercentage: number;
// Actions
refresh: () => Promise<void>;
markStepComplete: (stepNumber: number) => void;
clearError: () => void;
initializeOnboarding: () => void;
resetOnboarding: () => void;
}
const OnboardingContext = createContext<OnboardingContextValue | undefined>(undefined);
interface OnboardingProviderProps {
children: ReactNode;
}
export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({ children }) => {
const { isSignedIn, isLoaded: clerkLoaded } = useAuth();
const [data, setData] = useState<OnboardingData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
/**
* Fetch onboarding data from batch endpoint
*/
const fetchOnboardingData = useCallback(async () => {
// Don't fetch if not signed in
if (!isSignedIn) {
console.log('OnboardingContext: User not signed in, skipping fetch');
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Skip onboarding fetch in demo mode - onboarding is disabled
if (shouldSkipOnboarding()) {
console.log('OnboardingContext: Skipping onboarding fetch in demo mode');
setLoading(false);
return;
}
console.log('OnboardingContext: Fetching onboarding data for authenticated user...');
// Call batch init endpoint
const response = await apiClient.get('/api/onboarding/init');
const { user, onboarding, session } = response.data;
console.log('OnboardingContext: Data fetched successfully', {
user: user.id,
step: onboarding.current_step,
completed: onboarding.is_completed
});
// Update state
setData({ user, onboarding, session });
// Also cache in sessionStorage for backwards compatibility
sessionStorage.setItem('onboarding_init', JSON.stringify(response.data));
setLoading(false);
} catch (err) {
console.error('OnboardingContext: Error fetching data:', err);
// Check if it's a connection error that should be handled at the app level
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
// Re-throw connection errors to be handled by the app-level error boundary
throw err;
}
setError(err instanceof Error ? err.message : 'Failed to load onboarding data');
setLoading(false);
}
}, [isSignedIn]);
/**
* Initialize when Clerk auth is loaded and user is signed in
*/
useEffect(() => {
if (!clerkLoaded) {
console.log('OnboardingContext: Waiting for Clerk to load...');
return;
}
console.log('OnboardingContext: Clerk loaded, isSignedIn:', isSignedIn);
if (isSignedIn) {
console.log('OnboardingContext: User signed in, but waiting for subscription check...');
// Don't automatically fetch onboarding data - let InitialRouteHandler handle the flow
setLoading(false);
} else {
console.log('OnboardingContext: User not signed in, skipping data fetch');
setLoading(false);
}
}, [clerkLoaded, isSignedIn]);
// Separate effect to fetch data when explicitly requested
const initializeOnboarding = useCallback(() => {
if (isSignedIn && clerkLoaded) {
// Skip onboarding initialization in demo mode
if (shouldSkipOnboarding()) {
console.log('OnboardingContext: Skipping onboarding init in demo mode');
setLoading(false);
return;
}
console.log('OnboardingContext: Initializing onboarding data...');
fetchOnboardingData();
}
}, [isSignedIn, clerkLoaded, fetchOnboardingData]);
/**
* Refresh onboarding data (e.g., after completing a step)
*/
const refresh = useCallback(async () => {
console.log('OnboardingContext: Refreshing data...');
await fetchOnboardingData();
}, [fetchOnboardingData]);
/**
* Mark a step as complete (optimistic update + refresh)
*/
const markStepComplete = useCallback((stepNumber: number) => {
if (!data || !data.onboarding) return;
console.log(`OnboardingContext: Marking step ${stepNumber} as complete`);
// Optimistic update
setData(prevData => {
if (!prevData || !prevData.onboarding) return prevData;
const updatedSteps = prevData.onboarding.steps.map(step =>
step.step_number === stepNumber
? { ...step, status: 'completed' as const, completed_at: new Date().toISOString() }
: step
);
const completedSteps = updatedSteps.filter(s => s.status === 'completed' || s.status === 'skipped').length;
const completionPercentage = (completedSteps / updatedSteps.length) * 100;
return {
...prevData,
onboarding: {
is_completed: prevData.onboarding.is_completed,
current_step: Math.min(stepNumber + 1, updatedSteps.length),
completion_percentage: completionPercentage,
next_step: prevData.onboarding.next_step,
started_at: prevData.onboarding.started_at,
last_updated: new Date().toISOString(),
completed_at: prevData.onboarding.completed_at,
can_proceed_to_final: prevData.onboarding.can_proceed_to_final,
steps: updatedSteps
}
};
});
// Refresh from backend to ensure consistency
refresh();
}, [data, refresh]);
/**
* Clear error state
*/
const clearError = useCallback(() => {
setError(null);
}, []);
/**
* Reset onboarding progress and clear cache
*/
const resetOnboarding = useCallback(() => {
console.log('OnboardingContext: Resetting onboarding progress');
// Clear all cached data
sessionStorage.removeItem('onboarding_init');
localStorage.removeItem('onboarding_step');
localStorage.removeItem('onboarding_data');
// Reset state
setData(null);
setError(null);
setLoading(true);
// Re-fetch fresh data
fetchOnboardingData();
}, [fetchOnboardingData]);
/**
* Computed properties
*/
const isOnboardingComplete = data?.onboarding?.is_completed ?? false;
const currentStep = data?.onboarding?.current_step ?? 1;
const completionPercentage = data?.onboarding?.completion_percentage ?? 0;
const value: OnboardingContextValue = {
data,
loading,
error,
isOnboardingComplete,
currentStep,
completionPercentage,
refresh,
markStepComplete,
clearError,
initializeOnboarding,
resetOnboarding,
};
return (
<OnboardingContext.Provider value={value}>
{children}
</OnboardingContext.Provider>
);
};
/**
* Hook to use onboarding context
*
* Usage:
* const { data, loading, isOnboardingComplete, refresh } = useOnboarding();
*
* if (loading) return <Loading />;
* if (!isOnboardingComplete) return <Navigate to="/onboarding" />;
*/
export const useOnboarding = (): OnboardingContextValue => {
const context = useContext(OnboardingContext);
if (context === undefined) {
throw new Error('useOnboarding must be used within an OnboardingProvider');
}
return context;
};
/**
* Hook to safely use onboarding context (returns null if not in provider)
*
* Usage:
* const onboarding = useOnboardingOptional();
* if (onboarding) {
* // Use onboarding data
* }
*/
export const useOnboardingOptional = (): OnboardingContextValue | null => {
const context = useContext(OnboardingContext);
return context ?? null;
};
export default OnboardingContext;