import React, { useState, useEffect } from 'react'; import { User, Mail, Lock, Shield, Bell, Monitor, Clock, CheckCircle, AlertCircle, ChevronRight, Eye, EyeOff, Phone, MapPin, Plus, Trash2, Star, } from 'lucide-react'; import PhoneInput, { type Country, formatPhoneNumber } from 'react-phone-number-input'; import 'react-phone-number-input/style.css'; import { useCurrentUser } from '../hooks/useAuth'; import { useProfile, useUpdateProfile, useSendVerificationEmail, useChangePassword, useSessions, useRevokeOtherSessions, useSendPhoneVerification, useVerifyPhoneCode, useUserEmails, useAddUserEmail, useDeleteUserEmail, useSendUserEmailVerification, useSetPrimaryEmail, } from '../hooks/useProfile'; import type { UserEmail } from '../api/profile'; import TwoFactorSetup from '../components/profile/TwoFactorSetup'; import { useUserNotifications } from '../hooks/useUserNotifications'; interface SettingsSectionProps { title: string; description: string; icon: React.ReactNode; children: React.ReactNode; } const SettingsSection: React.FC = ({ title, description, icon, children }) => (
{icon}

{title}

{description}

{children}
); // Toast notification component const Toast: React.FC<{ message: string; type: 'success' | 'error'; onClose: () => void }> = ({ message, type, onClose, }) => { useEffect(() => { const timer = setTimeout(onClose, 3000); return () => clearTimeout(timer); }, [onClose]); return (
{type === 'success' ? : } {message}
); }; const ProfileSettings: React.FC = () => { const { data: currentUser, isLoading: userLoading } = useCurrentUser(); const { data: profile, isLoading: profileLoading, refetch: refetchProfile } = useProfile(); const updateProfile = useUpdateProfile(); const sendVerification = useSendVerificationEmail(); const sendPhoneVerification = useSendPhoneVerification(); const verifyPhoneCode = useVerifyPhoneCode(); const changePassword = useChangePassword(); const { data: sessions, isLoading: sessionsLoading } = useSessions(); const revokeOtherSessions = useRevokeOtherSessions(); // Multiple email hooks const { data: userEmails, isLoading: emailsLoading } = useUserEmails(); const addUserEmail = useAddUserEmail(); const deleteUserEmail = useDeleteUserEmail(); const sendUserEmailVerification = useSendUserEmailVerification(); const setPrimaryEmail = useSetPrimaryEmail(); const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'notifications'>('profile'); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); // WebSocket for real-time notifications (e.g., email verified) useUserNotifications({ onEmailVerified: (_emailId, email) => { setToast({ message: `Email "${email}" has been verified!`, type: 'success' }); }, }); const [show2FAModal, setShow2FAModal] = useState(false); // Form states const [name, setName] = useState(''); const [phone, setPhone] = useState(''); const [timezone, setTimezone] = useState('UTC'); const [locale, setLocale] = useState('en-US'); // Phone verification const [phoneVerificationCode, setPhoneVerificationCode] = useState(''); const [showPhoneVerification, setShowPhoneVerification] = useState(false); // Address fields const [addressLine1, setAddressLine1] = useState(''); const [addressLine2, setAddressLine2] = useState(''); const [city, setCity] = useState(''); const [state, setState] = useState(''); const [postalCode, setPostalCode] = useState(''); const [country, setCountry] = useState('US'); // Password form const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [showCurrentPassword, setShowCurrentPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); // Email management const [newEmail, setNewEmail] = useState(''); const [showAddEmail, setShowAddEmail] = useState(false); // Notification preferences const [notificationPrefs, setNotificationPrefs] = useState({ email: true, sms: false, in_app: true, appointment_reminders: true, marketing: false, }); // Initialize form data when profile loads useEffect(() => { if (profile) { setName(profile.name || ''); setPhone(profile.phone || ''); setTimezone(profile.timezone || 'UTC'); setLocale(profile.locale || 'en-US'); setAddressLine1(profile.address_line1 || ''); setAddressLine2(profile.address_line2 || ''); setCity(profile.city || ''); setState(profile.state || ''); setPostalCode(profile.postal_code || ''); setCountry(profile.country || 'US'); if (profile.notification_preferences) { setNotificationPrefs(profile.notification_preferences); } } }, [profile]); const showToast = (message: string, type: 'success' | 'error') => { setToast({ message, type }); }; const handleSaveProfile = async () => { try { await updateProfile.mutateAsync({ name, phone }); showToast('Profile updated successfully', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to update profile', 'error'); } }; const handleSendPhoneVerification = async () => { if (!phone) { showToast('Please enter a phone number first', 'error'); return; } try { await sendPhoneVerification.mutateAsync(phone); setShowPhoneVerification(true); showToast('Verification code sent! Check your console for the code.', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to send verification code', 'error'); } }; const handleVerifyPhoneCode = async () => { if (!phoneVerificationCode || phoneVerificationCode.length !== 6) { showToast('Please enter a 6-digit code', 'error'); return; } try { await verifyPhoneCode.mutateAsync(phoneVerificationCode); setShowPhoneVerification(false); setPhoneVerificationCode(''); showToast('Phone number verified successfully', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Invalid verification code', 'error'); } }; const handleSaveAddress = async () => { try { await updateProfile.mutateAsync({ address_line1: addressLine1, address_line2: addressLine2, city, state, postal_code: postalCode, country, }); showToast('Address updated successfully', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to update address', 'error'); } }; const handleSavePreferences = async () => { try { await updateProfile.mutateAsync({ timezone, locale }); showToast('Preferences updated successfully', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to update preferences', 'error'); } }; const handleSaveNotifications = async () => { try { await updateProfile.mutateAsync({ notification_preferences: notificationPrefs }); showToast('Notification preferences updated', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to update notifications', 'error'); } }; const handleSendVerification = async () => { try { await sendVerification.mutateAsync(); showToast('Verification email sent', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to send verification email', 'error'); } }; // Email management handlers const handleAddEmail = async () => { if (!newEmail || !newEmail.includes('@')) { showToast('Please enter a valid email address', 'error'); return; } try { await addUserEmail.mutateAsync(newEmail); setNewEmail(''); setShowAddEmail(false); showToast('Email added. Verification email sent.', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || err.response?.data?.email?.[0] || 'Failed to add email', 'error'); } }; const handleDeleteEmail = async (emailId: number) => { try { await deleteUserEmail.mutateAsync(emailId); showToast('Email removed', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to remove email', 'error'); } }; const handleSendEmailVerification = async (emailId: number) => { try { await sendUserEmailVerification.mutateAsync(emailId); showToast('Verification email sent', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to send verification email', 'error'); } }; const handleSetPrimaryEmail = async (emailId: number) => { try { await setPrimaryEmail.mutateAsync(emailId); showToast('Primary email updated', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to set primary email', 'error'); } }; const handleChangePassword = async () => { if (newPassword !== confirmPassword) { showToast('Passwords do not match', 'error'); return; } if (newPassword.length < 8) { showToast('Password must be at least 8 characters', 'error'); return; } try { await changePassword.mutateAsync({ currentPassword, newPassword }); showToast('Password changed successfully', 'success'); setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to change password', 'error'); } }; const handleRevokeOtherSessions = async () => { try { await revokeOtherSessions.mutateAsync(); showToast('All other sessions have been signed out', 'success'); } catch (err: any) { showToast(err.response?.data?.detail || 'Failed to revoke sessions', 'error'); } }; const handle2FASuccess = () => { refetchProfile(); showToast('Two-factor authentication updated', 'success'); }; if (userLoading || profileLoading) { return (
); } const user = profile || currentUser; if (!user) { return (

Unable to load user profile.

); } const tabs = [ { id: 'profile' as const, label: 'Profile', icon: }, { id: 'security' as const, label: 'Security', icon: }, { id: 'notifications' as const, label: 'Notifications', icon: }, ]; return (
{toast && setToast(null)} />} {show2FAModal && ( setShow2FAModal(false)} onSuccess={handle2FASuccess} onVerifyPhone={() => { setActiveTab('profile'); // Trigger phone verification after a short delay to allow tab switch setTimeout(() => { if (phone) { handleSendPhoneVerification(); } }, 100); }} /> )}

Profile Settings

Manage your account settings and preferences

{/* Tab Navigation */}
{tabs.map((tab) => ( ))}
{/* Profile Tab */} {activeTab === 'profile' && (
{/* Personal Information */} } >
setName(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
{/* Phone Verification */} } >

{phone ? formatPhoneNumber(phone) : 'No phone number set'}

{phone && (
{profile?.phone_verified ? ( <> Verified ) : ( <> Not verified )}
)}
null} value={phone} onChange={(value) => setPhone(value || '')} className="phone-input-wrapper" />
{showPhoneVerification ? (

Enter the 6-digit code sent to your phone. (Check Django console for code)

setPhoneVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="000000" maxLength={6} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent text-center text-xl tracking-widest" />
) : (
{phone && !profile?.phone_verified && ( )}
)}
{/* Address - Show for non-customer roles or when address is needed */} {profile?.role !== 'customer' && ( } >
setAddressLine1(e.target.value)} placeholder="Street address" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
setAddressLine2(e.target.value)} placeholder="Apartment, suite, etc. (optional)" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
setCity(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
setState(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
setPostalCode(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
)} {/* Email Settings */} } >

Any verified email address can be used to sign in to your account. Your primary email is used for account notifications.

{/* Email List */} {emailsLoading ? (
) : userEmails && userEmails.length > 0 ? (
{userEmails.map((email) => (

{email.email}

{email.is_primary && ( Primary )}
{email.verified ? ( <> Verified ) : ( <> Not verified )}
{!email.verified && ( )} {!email.is_primary && email.verified && ( )} {!email.is_primary && ( )}
))}
) : (

No email addresses found.

)} {/* Add Email Form */} {showAddEmail ? (
setNewEmail(e.target.value)} placeholder="Enter email address" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
) : ( )}
{/* Preferences */} } >
)} {/* Security Tab */} {activeTab === 'security' && (
{/* Password */} } >
setCurrentPassword(e.target.value)} className="w-full px-3 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
setNewPassword(e.target.value)} className="w-full px-3 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
setConfirmPassword(e.target.value)} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent" />
{/* Two-Factor Authentication */} } >

Authenticator App

{profile?.two_factor_enabled ? 'Enabled' : 'Not configured'}

{profile?.two_factor_enabled && (

Recovery Codes

Use these if you lose access to your authenticator

)}
{/* Active Sessions */} } >
{sessionsLoading ? (
) : sessions && sessions.length > 0 ? ( <> {sessions.map((session) => (

{session.is_current ? 'Current Session' : session.device_info || 'Unknown Device'}

{session.location || 'Unknown Location'} · Last active{' '} {session.is_current ? 'now' : new Date(session.last_activity).toLocaleDateString()}

{session.is_current && ( Active )}
))} ) : (

Current Session

This device · Last active now

Active
)}
)} {/* Notifications Tab */} {activeTab === 'notifications' && (
} >
{[ { id: 'email' as const, label: 'Email Notifications', description: 'Receive updates via email' }, { id: 'sms' as const, label: 'SMS Notifications', description: 'Receive text message alerts' }, { id: 'in_app' as const, label: 'In-App Notifications', description: 'Show notifications in the app' }, { id: 'appointment_reminders' as const, label: 'Appointment Reminders', description: 'Get reminded before appointments', }, { id: 'marketing' as const, label: 'Marketing Emails', description: 'Receive promotional content' }, ].map((pref) => (

{pref.label}

{pref.description}

))}
)}
); }; export default ProfileSettings;