import { createContext, useContext, useEffect, useState, type ReactNode, } from 'react' import { browserLocalPersistence, onAuthStateChanged, setPersistence, signInWithPopup, signInWithRedirect, getRedirectResult, signOut as firebaseSignOut, type User, } from 'firebase/auth' import { auth, googleProvider } from '../lib/firebase' import { registerUser, getUserByEmail } from '../lib/api' import { deriveSecretKey, generateDeviceKey, generateSalt, getSalt, saveSalt, getDeviceKey, saveDeviceKey, encryptSecretKey, decryptSecretKey, saveEncryptedSecretKey, getEncryptedSecretKey, } from '../lib/crypto' import { REMINDER_TIME_KEY, REMINDER_ENABLED_KEY } from '../hooks/useReminder' type MongoUser = { id: string email: string displayName?: string photoURL?: string theme?: string tutorial?: boolean backgroundImage?: string | null backgroundImages?: string[] reminder?: { enabled: boolean time?: string timezone?: string } } type AuthContextValue = { user: User | null userId: string | null mongoUser: MongoUser | null loading: boolean secretKey: Uint8Array | null authError: string | null signInWithGoogle: () => Promise signOut: () => Promise refreshMongoUser: () => Promise } const AuthContext = createContext(null) export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const [userId, setUserId] = useState(null) const [mongoUser, setMongoUser] = useState(null) const [secretKey, setSecretKey] = useState(null) const [loading, setLoading] = useState(true) const [authError, setAuthError] = useState(null) // Apply custom background image whenever mongoUser changes useEffect(() => { const bg = mongoUser?.backgroundImage if (bg) { document.body.style.backgroundImage = `url(${bg})` document.body.style.backgroundSize = 'cover' document.body.style.backgroundPosition = 'center' document.body.style.backgroundAttachment = 'fixed' document.body.classList.add('gj-has-bg') } else { document.body.style.backgroundImage = '' document.body.classList.remove('gj-has-bg') } }, [mongoUser?.backgroundImage]) // Initialize encryption keys on login async function initializeEncryption(authUser: User) { try { const firebaseUID = authUser.uid // Get or create salt let salt = getSalt() if (!salt) { salt = generateSalt() saveSalt(salt) } // Derive master key from Firebase UID (stable across sessions) const derivedKey = await deriveSecretKey(firebaseUID, salt) // Check if device key exists let deviceKey = await getDeviceKey() if (!deviceKey) { // First login on this device: generate device key deviceKey = await generateDeviceKey() await saveDeviceKey(deviceKey) } // Check if encrypted key exists in IndexedDB const cachedEncrypted = await getEncryptedSecretKey() if (!cachedEncrypted) { // First login (or IndexedDB cleared): encrypt and cache the key const encrypted = await encryptSecretKey(derivedKey, deviceKey) await saveEncryptedSecretKey(encrypted.ciphertext, encrypted.nonce) } else { // Subsequent login on same device: verify we can decrypt // (This ensures device key is correct) try { await decryptSecretKey( cachedEncrypted.ciphertext, cachedEncrypted.nonce, deviceKey ) } catch (error) { console.warn('Device key mismatch, regenerating...', error) // Device key doesn't match - regenerate deviceKey = await generateDeviceKey() await saveDeviceKey(deviceKey) const encrypted = await encryptSecretKey(derivedKey, deviceKey) await saveEncryptedSecretKey(encrypted.ciphertext, encrypted.nonce) } } // Keep secret key in memory for session setSecretKey(derivedKey) } catch (error) { console.error('Error initializing encryption:', error) throw error } } function syncReminderFromDb(mongoUser: MongoUser) { const r = mongoUser.reminder if (r) { localStorage.setItem(REMINDER_ENABLED_KEY, r.enabled ? 'true' : 'false') if (r.time) localStorage.setItem(REMINDER_TIME_KEY, r.time) else localStorage.removeItem(REMINDER_TIME_KEY) } else { localStorage.setItem(REMINDER_ENABLED_KEY, 'false') localStorage.removeItem(REMINDER_TIME_KEY) } } // Register or fetch user from MongoDB async function syncUserWithDatabase(authUser: User) { try { const token = await authUser.getIdToken() const email = authUser.email! // Initialize encryption before syncing user await initializeEncryption(authUser) // Try to get existing user try { // console.log('[Auth] Fetching user by email:', email) const existingUser = await getUserByEmail(email, token) as MongoUser setUserId(existingUser.id) setMongoUser(existingUser) syncReminderFromDb(existingUser) } catch (error) { console.warn('[Auth] User not found, registering...', error) const newUser = await registerUser( { email, displayName: authUser.displayName || undefined, photoURL: authUser.photoURL || undefined, }, token ) as MongoUser // console.log('[Auth] Registered new user:', newUser.id) setUserId(newUser.id) setMongoUser(newUser) syncReminderFromDb(newUser) } } catch (error) { console.error('[Auth] Error syncing user with database:', error) throw error } } useEffect(() => { // Handle returning from a redirect sign-in (Safari / iOS / Android WebViews) getRedirectResult(auth).catch((error) => { console.error('[Auth] Redirect sign-in error:', error) setAuthError(error instanceof Error ? error.message : 'Sign-in failed') }) // onAuthStateChanged below handles the successful redirect result automatically const unsubscribe = onAuthStateChanged(auth, async (u) => { setUser(u) if (u) { try { await syncUserWithDatabase(u) } catch (error) { console.error('Auth sync failed:', error) } } else { setUserId(null) setMongoUser(null) setSecretKey(null) } setLoading(false) }) return () => unsubscribe() }, []) async function signInWithGoogle() { setAuthError(null) await setPersistence(auth, browserLocalPersistence) // Safari blocks cross-origin storage in popups (ITP), so use redirect flow const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) if (isSafari) { await signInWithRedirect(auth, googleProvider) return } try { await signInWithPopup(auth, googleProvider) } catch (err: unknown) { const code = (err as { code?: string })?.code if (code === 'auth/popup-blocked') { // Popup was blocked (common on iOS Safari / Android WebViews) — fall back to redirect await signInWithRedirect(auth, googleProvider) } else { throw err } } } async function refreshMongoUser() { if (!user) return try { const token = await user.getIdToken() const email = user.email! const updated = await getUserByEmail(email, token) as MongoUser setMongoUser(updated) } catch (error) { console.error('[Auth] Error refreshing mongo user:', error) } } async function signOut() { setSecretKey(null) setMongoUser(null) localStorage.removeItem('gj-tour-pending-step') localStorage.removeItem(REMINDER_TIME_KEY) localStorage.removeItem(REMINDER_ENABLED_KEY) await firebaseSignOut(auth) setUserId(null) } const value: AuthContextValue = { user, userId, mongoUser, secretKey, loading, authError, signInWithGoogle, signOut, refreshMongoUser, } return ( {children} ) } export function useAuth() { const ctx = useContext(AuthContext) if (ctx == null) { throw new Error('useAuth must be used within AuthProvider') } return ctx }