import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { API_BASE_URL, TARGET_ENV, IS_WORDPRESS, ASSISTANT_ENABLED, ASSISTANT_BRAND_NAME, setAuthToken, clearAuthToken } from '../constants'; interface UserInfo { name: string; email: string; theme: 'system' | 'light' | 'dark'; seo_consent: boolean | null; } interface AuthContextProps { isLoggedIn: boolean; userInfo: UserInfo | null; login: (email: string, password: string) => Promise; logout: () => Promise; signup: (name: string, email: string, password: string, verificationCode: string) => Promise; updateEmail: (newEmail: string, currentPassword: string) => Promise<{ requiresVerification: boolean }>; updatePassword: (currentPassword: string, newPassword: string) => Promise; setUserInfo: (info: UserInfo) => void; authError: string | null; setAuthError: (error: string | null) => void; requestVerification: (email: string, purpose: 'registration' | 'password_reset' | 'email_change') => Promise; verifyCode: (email: string, code: string) => Promise; resetPassword: (email: string, code: string, newPassword: string) => Promise; // SEO Consent related properties seoConsent: boolean | null; hasDismissedBanner: boolean; setHasDismissedBanner: (value: boolean) => void; updateSeoConsent: (allowSeoUse: boolean) => Promise; } const AuthContext = createContext(undefined); interface AuthProviderProps { children: ReactNode; } export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [userInfo, setUserInfo] = useState(null); const [authError, setAuthError] = useState(null); const queryClient = useQueryClient(); // SEO Consent related state const [seoConsent, setSeoConsent] = useState(IS_WORDPRESS ? false : null); const [hasDismissedBanner, setHasDismissedBanner] = useState(true); const isPluginAuthBlocked = TARGET_ENV === 'wordpress' && !ASSISTANT_ENABLED; // Update SEO consent preference const updateSeoConsent = async (allowSeoUse: boolean) => { if (IS_WORDPRESS) { setSeoConsent(false); if (isLoggedIn && userInfo) { setUserInfo({ ...userInfo, seo_consent: false }); } try { document.cookie = `client_seo_consent=false; path=/; max-age=${365 * 24 * 60 * 60};`; } catch {} return; } try { // Update local state setSeoConsent(allowSeoUse); // Set client-side cookie directly document.cookie = `client_seo_consent=${allowSeoUse}; path=/; max-age=${365 * 24 * 60 * 60};`; // Send to server to set server-side cookie const response = await fetch(`${API_BASE_URL}/seo-consent`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ allow_seo_use: allowSeoUse }), }); if (response.ok) { // For logged-in users, update userInfo if (isLoggedIn && userInfo) { setUserInfo({ ...userInfo, seo_consent: allowSeoUse }); } } else { console.error('Failed to update server-side SEO consent'); } } catch (error) { console.error('Error updating SEO consent:', error); } }; // Initialize client-side cookie and state useEffect(() => { if (IS_WORDPRESS) { setSeoConsent(false); setHasDismissedBanner(true); try { document.cookie = `client_seo_consent=false; path=/; max-age=${365 * 24 * 60 * 60};`; } catch {} return; } // Check for cookies const clientConsentCookie = document.cookie .split('; ') .find(row => row.startsWith('client_seo_consent=')); const bannerDismissedCookie = document.cookie .split('; ') .find(row => row.startsWith('seo_banner_dismissed=')); // --- Part 1: Determine Consent State --- let consentValue: boolean | null = null; let isNewUser = false; if (clientConsentCookie) { consentValue = clientConsentCookie.split('=')[1] === 'true'; } else if (isLoggedIn && userInfo && userInfo.seo_consent !== null) { consentValue = userInfo.seo_consent; // Sync user profile setting to cookie for consistency document.cookie = `client_seo_consent=${consentValue}; path=/; max-age=${365 * 24 * 60 * 60};`; } else { // This is a new user/session. Default to opt-in. consentValue = true; isNewUser = true; // Flag to sync with backend } setSeoConsent(consentValue); // If it's a new user, set the default on the backend as well if (isNewUser) { const initializeBackendConsent = async (value: boolean) => { // Set client-side cookie immediately for non-logged-in users if (!isLoggedIn) { document.cookie = `client_seo_consent=${value}; path=/; max-age=${365 * 24 * 60 * 60};`; } try { await fetch(`${API_BASE_URL}/seo-consent`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ allow_seo_use: value }), }); } catch (error) { console.error('Failed to set default SEO consent on backend:', error); } }; initializeBackendConsent(consentValue); } // --- Part 2: Determine Banner Visibility --- // The banner should only be shown once. If it has been dismissed, never show again. // Also, if a logged-in user has a preference, they've already "seen" it. if (bannerDismissedCookie) { setHasDismissedBanner(true); } else if (isLoggedIn && userInfo && userInfo.seo_consent !== null) { setHasDismissedBanner(true); } else { setHasDismissedBanner(false); } }, [isLoggedIn, userInfo]); // Check authentication status on mount useEffect(() => { const checkAuthStatus = async () => { if (isPluginAuthBlocked) { setIsLoggedIn(false); setUserInfo(null); return; } // console.log('checking auth status'); let hasRetried = false; const fetchStatus = async () => { try { const response = await fetch(`${API_BASE_URL}/auth-status`, { method: 'GET', credentials: 'include', }); if (response.status === 401 && !hasRetried) { hasRetried = true; console.log('Received 401. Retrying auth status check...'); return await fetchStatus(); } if (response.ok) { const data = await response.json(); setIsLoggedIn(data.isLoggedIn); } else { setIsLoggedIn(false); } } catch (error) { console.error('Error checking auth status:', error); setIsLoggedIn(false); } }; await fetchStatus(); }; checkAuthStatus(); }, [isPluginAuthBlocked]); // Fetch user info when logged in const fetchUserInfo = useCallback(async () => { if (isPluginAuthBlocked) { setIsLoggedIn(false); setUserInfo(null); return; } if (!isLoggedIn) return; try { const response = await fetch(`${API_BASE_URL}/user-info`, { method: 'GET', credentials: 'include', }); if (response.ok) { const data = await response.json(); setUserInfo({ name: data.name, email: data.email, theme: data.theme, seo_consent: data.seo_consent, }); if (!IS_WORDPRESS) { // Update theme cookie document.cookie = `theme=${data.theme}; path=/;`; } } else { setIsLoggedIn(false); setUserInfo(null); } } catch (error) { console.error('Error fetching user info:', error); setIsLoggedIn(false); setUserInfo(null); } }, [isLoggedIn, isPluginAuthBlocked]); useEffect(() => { fetchUserInfo(); }, [fetchUserInfo]); // Login function const login = useCallback( async (email: string, password: string) => { if (isPluginAuthBlocked) { const message = `Enable ${ASSISTANT_BRAND_NAME} in settings to use login and signup.`; setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/login`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to log in'); throw new Error(errorData.detail || 'Failed to log in'); } // Store bearer token in plugin mode to avoid 3rd-party cookie issues if (TARGET_ENV === 'wordpress') { try { const data = await response.json(); if (data?.access_token) setAuthToken(data.access_token); } catch {} } queryClient.clear(); window.location.reload(); } catch (error) { console.error('Error during login:', error); throw error; } }, [API_BASE_URL, queryClient, fetchUserInfo, isPluginAuthBlocked] ); // Logout function const logout = useCallback( async () => { if (isPluginAuthBlocked) { clearAuthToken(); setIsLoggedIn(false); setUserInfo(null); queryClient.clear(); return; } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/logout`, { method: 'POST', credentials: 'include', }); if (!IS_WORDPRESS) { // Remove the theme cookie document.cookie = `theme=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;`; } // Remove the client-side SEO consent cookie document.cookie = `client_seo_consent=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;`; if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to log out'); throw new Error(errorData.detail || 'Failed to log out'); } // Clear stored bearer token in plugin mode if (TARGET_ENV === 'wordpress') { clearAuthToken(); } queryClient.clear(); window.location.reload(); } catch (error) { console.error('Error during logout:', error); throw error; } }, [API_BASE_URL, queryClient, isPluginAuthBlocked] ); // Signup function const signup = useCallback( async (name: string, email: string, password: string, verificationCode: string) => { if (isPluginAuthBlocked) { const message = `Enable ${ASSISTANT_BRAND_NAME} in settings to use login and signup.`; setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/register`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user: { name, email, password, }, verification: { email, code: verificationCode } }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Registration failed'); } if (TARGET_ENV === 'wordpress') { try { const data = await response.json(); if (data?.access_token) setAuthToken(data.access_token); } catch {} } queryClient.clear(); window.location.reload(); } catch (error) { const message = error instanceof Error ? error.message : 'Registration failed'; setAuthError(message); throw error; } }, [API_BASE_URL, queryClient, fetchUserInfo, isPluginAuthBlocked] ); // Update Email const updateEmail = useCallback( async (newEmail: string, currentPassword: string) => { if (isPluginAuthBlocked) { const message = `Enable ${ASSISTANT_BRAND_NAME} in settings to use account features.`; setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/account/email`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_email: newEmail, current_password: currentPassword }), }); if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to update email'); throw new Error(errorData.detail || 'Failed to update email'); } const data = await response.json(); // Only update user info immediately if verification is not required if (!data.requires_verification) { await fetchUserInfo(); } return { requiresVerification: data.requires_verification }; } catch (error) { console.error('Error updating email:', error); throw error; } }, [API_BASE_URL, fetchUserInfo, isPluginAuthBlocked] ); // Update Password const updatePassword = useCallback( async (currentPassword: string, newPassword: string) => { if (isPluginAuthBlocked) { const message = `Enable ${ASSISTANT_BRAND_NAME} in settings to use account features.`; setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/account/password`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }), }); if (!response.ok) { const errorData = await response.json(); setAuthError(errorData.detail || 'Failed to update password'); throw new Error(errorData.detail || 'Failed to update password'); } await fetchUserInfo(); } catch (error) { console.error('Error updating password:', error); throw error; } }, [API_BASE_URL, fetchUserInfo, isPluginAuthBlocked] ); // Send verification code const requestVerification = useCallback( async (email: string, purpose: 'registration' | 'password_reset' | 'email_change') => { if (isPluginAuthBlocked) { const message = `Enable ${ASSISTANT_BRAND_NAME} in settings to use login and signup.`; setAuthError(message); throw new Error(message); } try { const response = await fetch(`${API_BASE_URL}/request-verification`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.toLowerCase(), purpose: purpose }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to send verification code'); } } catch (error) { const message = error instanceof Error ? error.message : 'Failed to request verification'; console.error('Error requesting verification:', message); throw new Error(message); } }, [API_BASE_URL, isPluginAuthBlocked] ); // Verify code const verifyCode = useCallback(async (email: string, code: string) => { if (isPluginAuthBlocked) { const message = `Enable ${ASSISTANT_BRAND_NAME} in settings to use account features.`; setAuthError(message); return false; } try { const response = await fetch(`${API_BASE_URL}/account/email/verify`, { method: 'PUT', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code }), }); const data = await response.json(); if (!response.ok) { setAuthError(data.detail || 'Failed to verify code'); throw new Error(data.detail || 'Failed to verify code'); } await fetchUserInfo(); return true; } catch (error) { const message = error instanceof Error ? error.message : 'Verification failed'; setAuthError(message); return false; } }, [API_BASE_URL, fetchUserInfo, isPluginAuthBlocked]); const resetPassword = useCallback( async (email: string, code: string, newPassword: string) => { if (isPluginAuthBlocked) { const message = `Enable ${ASSISTANT_BRAND_NAME} in settings to use login and signup.`; setAuthError(message); throw new Error(message); } setAuthError(null); try { const response = await fetch(`${API_BASE_URL}/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code, new_password: newPassword }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Password reset failed'); } return true; } catch (error) { const message = error instanceof Error ? error.message : 'Password reset failed'; setAuthError(message); throw error; } }, [API_BASE_URL, isPluginAuthBlocked] ); const value: AuthContextProps = { isLoggedIn, userInfo, login, logout, setAuthError, signup, updateEmail, updatePassword, setUserInfo, authError, requestVerification, verifyCode, resetPassword, // SEO Consent related properties seoConsent, hasDismissedBanner, setHasDismissedBanner: (value: boolean) => { setHasDismissedBanner(value); if (value && !IS_WORDPRESS) { document.cookie = `seo_banner_dismissed=true; path=/; max-age=${365 * 24 * 60 * 60}; SameSite=Lax`; } }, updateSeoConsent, }; return {children}; }; export const useAuth = (): AuthContextProps => { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; };