/** * Onboarding Screen Component */ import React, { useState } from 'react'; import { __ } from '@wordpress/i18n'; import Select from 'react-select'; import { useContentPlan } from '../context/ContentPlanContext'; import ErrorMessage from './ErrorMessage'; interface OnboardingScreenProps { onComplete: () => void; onSkip: () => void; } const OnboardingScreen: React.FC = ({ onComplete, onSkip, }) => { const { getString, saveSettings, updateSettings, isNewWebsite, generateContentPlan, createMultipleContentPlanItems, setCurrentTaskId, fetchTasks, } = useContentPlan(); const [formData, setFormData] = useState({ goal: 'traffic' as string, // Changed to single selection language: 'English', termsAccepted: false, customRecommendations: '', }); const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [isSubmitting, setIsSubmitting] = useState(false); // Available goals with descriptions const goalOptions = [ { value: 'traffic', label: getString('generation_modal', 'goal_traffic', 'Traffic'), description: getString( 'onboarding', 'goal_traffic_description', 'Grow your organic reach and page views' ), }, { value: 'leads', label: getString('generation_modal', 'goal_leads', 'Leads'), description: getString( 'onboarding', 'goal_leads_description', 'Generate qualified leads and conversions' ), }, { value: 'authority', label: getString('generation_modal', 'goal_authority', 'Authority'), description: getString( 'onboarding', 'goal_authority_description', 'Establish expertise and thought leadership' ), }, ]; // Available languages (same as ContentPlanGenerationModal) const languageOptions = [ { value: 'English', label: 'English' }, { value: 'Spanish', label: 'Spanish' }, { value: 'French', label: 'French' }, { value: 'German', label: 'German' }, { value: 'Italian', label: 'Italian' }, { value: 'Portuguese', label: 'Portuguese' }, { value: 'Dutch', label: 'Dutch' }, { value: 'Russian', label: 'Russian' }, { value: 'Chinese (Simplified)', label: 'Chinese (Simplified)' }, { value: 'Chinese (Traditional)', label: 'Chinese (Traditional)' }, { value: 'Japanese', label: 'Japanese' }, { value: 'Korean', label: 'Korean' }, { value: 'Arabic', label: 'Arabic' }, { value: 'Hebrew', label: 'Hebrew' }, { value: 'Turkish', label: 'Turkish' }, { value: 'Persian (Farsi)', label: 'Persian (Farsi)' }, { value: 'Urdu', label: 'Urdu' }, { value: 'Hindi', label: 'Hindi' }, { value: 'Bengali', label: 'Bengali' }, { value: 'Punjabi', label: 'Punjabi' }, { value: 'Marathi', label: 'Marathi' }, { value: 'Gujarati', label: 'Gujarati' }, { value: 'Tamil', label: 'Tamil' }, { value: 'Telugu', label: 'Telugu' }, { value: 'Kannada', label: 'Kannada' }, { value: 'Malayalam', label: 'Malayalam' }, { value: 'Sinhala', label: 'Sinhala' }, { value: 'Nepali', label: 'Nepali' }, { value: 'Vietnamese', label: 'Vietnamese' }, { value: 'Thai', label: 'Thai' }, { value: 'Indonesian', label: 'Indonesian' }, { value: 'Malay', label: 'Malay' }, { value: 'Filipino (Tagalog)', label: 'Filipino (Tagalog)' }, { value: 'Polish', label: 'Polish' }, { value: 'Czech', label: 'Czech' }, { value: 'Slovak', label: 'Slovak' }, { value: 'Hungarian', label: 'Hungarian' }, { value: 'Romanian', label: 'Romanian' }, { value: 'Bulgarian', label: 'Bulgarian' }, { value: 'Ukrainian', label: 'Ukrainian' }, { value: 'Serbian', label: 'Serbian' }, { value: 'Croatian', label: 'Croatian' }, { value: 'Slovenian', label: 'Slovenian' }, { value: 'Greek', label: 'Greek' }, { value: 'Swedish', label: 'Swedish' }, { value: 'Norwegian', label: 'Norwegian' }, { value: 'Danish', label: 'Danish' }, { value: 'Finnish', label: 'Finnish' }, { value: 'Icelandic', label: 'Icelandic' }, { value: 'Lithuanian', label: 'Lithuanian' }, { value: 'Latvian', label: 'Latvian' }, { value: 'Estonian', label: 'Estonian' }, { value: 'Albanian', label: 'Albanian' }, { value: 'Armenian', label: 'Armenian' }, { value: 'Georgian', label: 'Georgian' }, { value: 'Afrikaans', label: 'Afrikaans' }, { value: 'Swahili', label: 'Swahili' }, ]; const handleGoalChange = (goalValue: string) => { setFormData(prev => ({ ...prev, goal: goalValue, })); if (errors.goal) { setErrors(prev => ({ ...prev, goal: '' })); } }; const handleLanguageChange = (selectedOption: any) => { const value = selectedOption?.value || 'English'; setFormData(prev => ({ ...prev, language: value })); if (errors.language) { setErrors(prev => ({ ...prev, language: '' })); } }; const handleTermsChange = (checked: boolean) => { setFormData(prev => ({ ...prev, termsAccepted: checked })); if (errors.termsAccepted) { setErrors(prev => ({ ...prev, termsAccepted: '' })); } }; const handleCustomRecommendationsChange = (value: string) => { setFormData(prev => ({ ...prev, customRecommendations: value })); }; const validateForm = (): boolean => { const newErrors: { [key: string]: string } = {}; if (!formData.goal) { newErrors.goal = getString( 'onboarding', 'error_goal_required', 'Please select a goal' ); } if (!formData.language) { newErrors.language = getString( 'onboarding', 'error_language_required', 'Please select a language' ); } if (!formData.termsAccepted) { newErrors.termsAccepted = getString( 'onboarding', 'error_terms_required', 'You must agree to the Terms of Use and Privacy Policy' ); } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { return; } setIsSubmitting(true); // Clear any previous errors setErrors({}); // Track if all operations completed successfully let allOperationsSucceeded = false; try { // Save onboarding form data WITHOUT marking as completed yet // We'll mark it as completed only after successful generation await saveSettings({ goals: [formData.goal], // Convert single goal to array format language: formData.language, customRecommendations: formData.customRecommendations || '', // NOTE: onboarding_completed will be set to true AFTER successful generation }); // Prepare generation form data (same format as ContentPlanGenerationModal) const generationFormData = { goals: [formData.goal], publishInterval: 7, // Default: Publish every 7 days totalPosts: 5, // Generate 5 posts for onboarding (as mentioned in hero text) language: formData.language, customLanguage: '', customRecommendations: formData.customRecommendations || '', }; // Call the context function which will handle the API call and return generated items let result; try { result = await generateContentPlan(generationFormData); } catch (genError: any) { // Re-throw generation errors to be handled by outer catch block console.error( 'OnboardingScreen: generateContentPlan threw error:', genError ); throw genError; } // Defensive check: ensure result is valid and doesn't contain error indicators if (!result) { throw new Error( getString( 'onboarding', 'error_submit_failed', 'Failed to complete onboarding: No response from server' ) ); } // Check for error indicators in result (defensive check) const resultAny = result as any; if (resultAny.error || resultAny.status_code >= 400) { const errorMessage = resultAny.error || resultAny.message || (resultAny.validation_errors && typeof resultAny.validation_errors === 'object' ? Object.values(resultAny.validation_errors)[0] : null) || getString( 'onboarding', 'error_submit_failed', 'Failed to complete onboarding' ); throw new Error( typeof errorMessage === 'string' ? errorMessage : 'Failed to generate content plan' ); } const generatedItems = result.items; const latestDateFromBackend = result.latestScheduledDate; const taskId = result.taskId; // If taskId is present, set it in context (tasks panel will show in main view) if (taskId) { setCurrentTaskId(taskId); // Fetch tasks immediately to get the latest status // Don't await - let it run in background so onboarding can complete quickly fetchTasks({ limit: 20 }).catch(err => { console.error('Error fetching tasks:', err); }); // Also fetch again after a delay to catch quick failures (e.g., moderation blocks) setTimeout(() => { fetchTasks({ limit: 20 }).catch(err => { console.error('Error fetching tasks (delayed):', err); }); }, 1500); } else if (generatedItems && generatedItems.length > 0) { // Automatically add all generated items to the plan await createMultipleContentPlanItems( generatedItems, generationFormData.publishInterval, 1, // planId latestDateFromBackend ); } // Mark all operations as successful allOperationsSucceeded = true; // NOW mark onboarding as completed since generation succeeded await saveSettings({ onboarding_completed: true, }); // Only call onComplete callback after ALL operations succeed // This ensures the onboarding popup only closes on successful backend response onComplete(); } catch (error: any) { console.error('Failed to complete onboarding:', error); console.error('Error object structure:', { error, errorData: error?.data, errorResponse: error?.response, errorMessage: error?.message, }); // Extract error message from error object // WordPress apiFetch may throw errors with response data nested in error.data or error.response let errorMsg = 'Unknown error'; if (error && typeof error === 'object') { // Check multiple possible locations for error data const responseData = error.data || error.response || error.response?.data || error; // Check for error field in response (highest priority) if (responseData.error) { errorMsg = responseData.error; } // Check if error has a message property else if (error.message) { errorMsg = error.message; } // Check if error has validation_errors else if ( responseData.validation_errors && typeof responseData.validation_errors === 'object' ) { const validationErrors = responseData.validation_errors; const firstError = Object.values(validationErrors)[0]; errorMsg = typeof firstError === 'string' ? firstError : 'Validation error'; } // Check for message in response data else if (responseData.message) { errorMsg = responseData.message; } } else if (typeof error === 'string') { errorMsg = error; } // Check if this is a connection error and replace with friendly message const connectionErrorPatterns = [ 'cURL error 7', 'Failed to connect', "Couldn't connect to server", 'Connection refused', 'Connection timeout', 'Network error', ]; const isConnectionError = connectionErrorPatterns.some(pattern => errorMsg.toLowerCase().includes(pattern.toLowerCase()) ); if (isConnectionError) { errorMsg = getString( 'generation_modal', 'connection_error', "🚀 Houston, we have a connection problem.\n\nWe can't reach our backend right now.\n\nPlease try again in a moment or contact support if this keeps happening." ); } // Set error and keep popup open (don't call onComplete) console.error( 'OnboardingScreen: Error occurred, keeping popup open. Error:', errorMsg ); setErrors({ submit: errorMsg || getString( 'onboarding', 'error_submit_failed', 'Failed to complete onboarding' ), }); // Ensure onComplete is NOT called on error if (allOperationsSucceeded) { console.warn( 'OnboardingScreen: WARNING - allOperationsSucceeded was true but error occurred!' ); } } finally { setIsSubmitting(false); } }; const handleSkip = async () => { try { // Prepare complete onboarding settings with current form values const onboardingSettings = { onboarding_completed: true, goals: [formData.goal], // Convert single goal to array format language: formData.language, customRecommendations: formData.customRecommendations || '', }; // Optimistically update settings in context - this will trigger re-render and show dashboard immediately updateSettings(onboardingSettings); // Save complete onboarding settings to backend in the background (fire and forget) // Don't await - let it happen in background while dashboard is already showing saveSettings(onboardingSettings).catch(error => { console.error('Failed to save onboarding skip:', error); // On error, we could optionally revert the optimistic update, but for UX we'll keep it }); } catch (error) { console.error('Failed to skip onboarding:', error); // Even on error, update optimistically to show dashboard updateSettings({ onboarding_completed: true, goals: [formData.goal], language: formData.language, customRecommendations: formData.customRecommendations || '', }); } }; return (
{/* Hero Section */}

{getString( 'onboarding', 'hero_title', 'Create your first 5 posts completely free.' )}

{getString( 'onboarding', 'hero_subtitle', 'No API keys or external setup required - everything works out of the box.' )}

{/* Visual Illustration */}
{getString(
{/* Benefits List - Compact 2-column layout */}
  • {getString( 'onboarding', 'benefit_content_plan', 'Automatically generate a multi-month content plan' )}
  • {getString( 'onboarding', 'benefit_seo_posts', 'High-quality SEO-optimized posts' )}
  • {getString( 'onboarding', 'benefit_editors', 'Works with Gutenberg, Elementor, and Classic Editor' )}
  • {getString( 'onboarding', 'benefit_no_setup', 'No technical setup needed' )}
{/* Form Card Container */}
{/* Goals - Radio buttons with descriptions */}
{goalOptions.map(goal => ( ))}
{errors.goal && (

{errors.goal}

)}
{/* Language */}