import React, { useState, useEffect } from 'react' import { Eye, EyeOff, ArrowLeft } from 'lucide-react' import ProfileLogo from '../Logos/ProfileLogo' import { useTheme } from '../../contexts/ThemeContext'; import { useIsSettingsModalOpen, useSetIsSettingsModalOpen } from '../../contexts/UIContext'; import { useAuth } from '../../contexts/AuthContext'; import { IS_WORDPRESS } from '../../constants'; import useRenderTracker from '../../hooks/useRenderTracker'; const spinnerProps = { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [ , ] }; export default function SettingsModal() { const isSettingsModalOpen = useIsSettingsModalOpen(); const setIsSettingsModalOpen = useSetIsSettingsModalOpen(); const { theme, setTheme } = useTheme(); const { userInfo, logout, updateEmail, updatePassword, requestVerification, verifyCode } = useAuth(); const [activeTab, setActiveTab] = useState<'profile' | 'preferences'>('profile') const [isEditingEmail, setIsEditingEmail] = useState(false) const [isEditingPassword, setIsEditingPassword] = useState(false) const [newEmail, setNewEmail] = useState('') const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [selectedTheme, setSelectedTheme] = useState(theme) const [showCurrentPassword, setShowCurrentPassword] = useState(false) const [showNewPassword, setShowNewPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) const [emailCurrentPassword, setEmailCurrentPassword] = useState('') const [showEmailCurrentPassword, setShowEmailCurrentPassword] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false); const [successMessage, setSuccessMessage] = useState(null); const [verificationCode, setVerificationCode] = useState(''); const [emailVerificationStep, setEmailVerificationStep] = useState<'initial' | 'verifying' | 'complete'>('initial'); const [emailChangeData, setEmailChangeData] = useState<{ newEmail: string; currentPassword: string; } | null>(null); const [emailError, setEmailError] = useState(null); const [verificationError, setVerificationError] = useState(null); const [infoMessage, setInfoMessage] = useState(null); const [passwordError, setPasswordError] = useState(null); useRenderTracker('SettingsModal', { isSettingsModalOpen, activeTab, isEditingEmail, isEditingPassword, selectedTheme, emailCurrentPassword, newEmail, currentPassword, newPassword, confirmPassword, showCurrentPassword, showNewPassword, showConfirmPassword, userInfo, useIsSettingsModalOpen, useSetIsSettingsModalOpen, theme, setTheme, logout, updateEmail, updatePassword, requestVerification, verifyCode, }); // Update selectedTheme when userInfo.theme changes useEffect(() => { setSelectedTheme(theme); }, [theme]); if (!isSettingsModalOpen) return null const resetState = () => { setActiveTab('profile'); resetEmailChangeState(); setIsEditingEmail(false); setIsEditingPassword(false); setEmailCurrentPassword(''); setNewEmail(''); setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); setShowCurrentPassword(false); setShowNewPassword(false); setShowConfirmPassword(false); setShowEmailCurrentPassword(false); setVerificationCode(''); setEmailVerificationStep('initial'); setEmailChangeData(null); setEmailError(null); setVerificationError(null); setInfoMessage(null); setSuccessMessage(null); }; const handleClose = () => { resetState(); setIsSettingsModalOpen(false); }; const handleEmailUpdate = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); setEmailError(null); setSuccessMessage(null); try { const result = await updateEmail(newEmail, emailCurrentPassword); if (result.requiresVerification) { await requestVerification(newEmail, 'email_change'); setEmailChangeData({ newEmail, currentPassword: emailCurrentPassword }); setEmailVerificationStep('verifying'); } } catch (error) { setEmailError(error instanceof Error ? error.message : 'Failed to update email'); } finally { setIsSubmitting(false); } }; // Add validation functions const validatePassword = (password: string): string | null => { if (password.length < 8) { return 'Password must be at least 8 characters long'; } return null; }; const handlePasswordUpdate = async (e: React.FormEvent) => { e.preventDefault(); setPasswordError(null); setSuccessMessage(null); // Client-side validation const passwordValidationError = validatePassword(newPassword); if (passwordValidationError) { setPasswordError(passwordValidationError); return; } if (newPassword !== confirmPassword) { setPasswordError('New passwords do not match'); return; } setIsSubmitting(true); try { await updatePassword(currentPassword, newPassword); resetState(); setSuccessMessage('Password updated successfully!'); } catch (error) { // Improved error handling if (error instanceof Error) { setPasswordError(error.message); } else if (typeof error === 'object' && error !== null) { // Handle validation errors from the server const errorObj = error as { detail?: string }; setPasswordError(errorObj.detail || 'Failed to update password'); } else { setPasswordError('An unexpected error occurred'); } } finally { setIsSubmitting(false); } }; const handleThemeUpdate = async (newTheme: 'system' | 'light' | 'dark') => { setSelectedTheme(newTheme) setTheme(newTheme) } const handleVerificationSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); setVerificationError(null); try { if (!emailChangeData) throw new Error('Missing email change data'); const isValid = await verifyCode(emailChangeData.newEmail, verificationCode); if (isValid) { resetState(); setSuccessMessage('Email updated successfully!'); } } catch (error) { setVerificationError(error instanceof Error ? error.message : 'Verification failed'); } finally { setIsSubmitting(false); } }; const resetEmailChangeState = () => { setIsEditingEmail(false); setEmailVerificationStep('initial'); setEmailChangeData(null); setSuccessMessage(null); setEmailError(null); setVerificationError(null); setInfoMessage(null); setNewEmail(''); setEmailCurrentPassword(''); setVerificationCode(''); }; return ( Settings {!isEditingEmail && !isEditingPassword && ( setActiveTab('profile')} > Account setActiveTab('preferences')} > Preferences )} {activeTab === 'profile' && ( {!isEditingEmail && !isEditingPassword && ( <> {userInfo?.name} {userInfo?.email} setIsEditingEmail(true)} className="mt-4 px-3 py-2 text-xs font-medium rounded-3xl bg-gray-800 text-background-light dark:text-background-dark dark:hover:bg-gray-300/85 dark:bg-gray-300 dark:border-gray-700 shadow-sm dark:shadow-md dark:shadow-black/50 hover:shadow-none dark:hover:shadow-none hover:bg-gray-800/85" > Change Email setIsEditingPassword(true)} className="mt-4 px-3 py-2 text-xs font-medium rounded-3xl text-gray-600 dark:text-gray-300 dark:hover:bg-gray-700/30 bg-background-light dark:bg-background-dark border border-gray-200 dark:border-gray-700 shadow-sm dark:shadow-md dark:shadow-black/50 hover:shadow-none dark:hover:shadow-none hover:bg-gray-100" > Change Password Log Out > )} {isEditingEmail && ( resetState()} className="mr-3 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors" aria-label="Go back" > Change Email {emailVerificationStep === 'verifying' && ( We've sent a verification code to {emailChangeData?.newEmail} Please check your inbox (and spam folder) for the code. )} {emailVerificationStep === 'initial' ? ( Current Password setEmailCurrentPassword(e.target.value)} className="w-full px-4 py-3 rounded-2xl border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-1 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-500/15 text-gray-900 dark:text-gray-100" required /> setShowEmailCurrentPassword(!showEmailCurrentPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400" aria-label={showEmailCurrentPassword ? "Hide current password" : "Show current password"} > {showEmailCurrentPassword ? : } New Email setNewEmail(e.target.value)} className="w-full px-4 py-3 rounded-2xl border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-1 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-500/15 text-gray-900 dark:text-gray-100" required /> {emailError && ( {emailError} )} {isSubmitting ? ( Updating... ) : ( 'Update Email' )} ) : emailVerificationStep === 'verifying' && ( Verification Code setVerificationCode(e.target.value)} className="w-full px-4 py-3 rounded-2xl border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-1 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-500/15 text-gray-900 dark:text-gray-100" required /> {verificationError && ( {verificationError} )} {isSubmitting ? ( Verifying... ) : ( 'Verify Code' )} )} {infoMessage && ( {infoMessage} )} )} {isEditingPassword && ( resetState()} className="mr-3 p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors" aria-label="Go back" > Change Password Current Password setCurrentPassword(e.target.value)} className="w-full px-4 py-3 rounded-2xl border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-1 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-500/15 text-gray-900 dark:text-gray-100" required /> setShowCurrentPassword(!showCurrentPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400" aria-label={showCurrentPassword ? "Hide current password" : "Show current password"} > {showCurrentPassword ? : } New Password { setNewPassword(e.target.value); }} className="w-full px-4 py-3 rounded-2xl border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-1 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-500/15 text-gray-900 dark:text-gray-100" required /> setShowNewPassword(!showNewPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400" aria-label={showNewPassword ? "Hide new password" : "Show new password"} > {showNewPassword ? : } Confirm New Password setConfirmPassword(e.target.value)} className="w-full px-4 py-3 rounded-2xl border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-1 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-500/15 text-gray-900 dark:text-gray-100" required /> setShowConfirmPassword(!showConfirmPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400" aria-label={showConfirmPassword ? "Hide confirm password" : "Show confirm password"} > {showConfirmPassword ? : } {passwordError && ( {passwordError} )} {isSubmitting ? ( Updating... ) : ( 'Update Password' )} )} )} {activeTab === 'preferences' && ( Theme {['system', 'light', 'dark'].map((theme) => ( handleThemeUpdate(theme as 'system' | 'light' | 'dark')} className={`py-2 px-4 rounded-2xl text-sm ${ selectedTheme === theme ? 'bg-gray-800 dark:bg-gray-100 text-gray-100 dark:text-background-dark' : 'bg-gray-100 text-gray-700 dark:bg-background-dark dark:text-gray-300 border border-gray-300 dark:border-gray-800 shadow dark:shadow-md dark:shadow-gray-800/30 hover:shadow-none dark:hover:shadow-none hover:bg-gray-800 hover:border-gray-800 dark:hover:border-gray-100 dark:hover:bg-gray-100 dark:hover:text-background-dark hover:text-gray-100 duration-0' }`} > {theme.charAt(0).toUpperCase() + theme.slice(1)} ))} {!IS_WORDPRESS && ( Content Publishing Allow anonymized conversations to be published as WordPress articles )} )} {successMessage && ( {successMessage} )} ) } const SeoConsentToggle = () => { const { userInfo, updateSeoConsent } = useAuth(); const [isEnabled, setIsEnabled] = useState(() => userInfo?.seo_consent === true); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { if (userInfo !== undefined) { setIsEnabled(userInfo?.seo_consent === true); } }, [userInfo]); if (IS_WORDPRESS) { return null; } const toggleConsent = async () => { if (isSubmitting) return; setIsSubmitting(true); const currentValue = isEnabled === true; const newValue = !currentValue; try { await updateSeoConsent(newValue); setIsEnabled(newValue); } catch (error) { console.error("Failed to update consent:", error); } finally { setIsSubmitting(false); } }; if (!userInfo) return ; return ( {isEnabled ? 'Enable' : 'Disable'} content publishing ); };
{userInfo?.email}
We've sent a verification code to {emailChangeData?.newEmail}
Please check your inbox (and spam folder) for the code.
{infoMessage}
Allow anonymized conversations to be published as WordPress articles