looood / src /hooks /use-auth.ts
looda3131's picture
Clean push without any binary history
cc276cc
"use client";
import { useState, useCallback, useEffect } from 'react';
import { onAuthStateChanged, signInAnonymously, User as FirebaseUser, signOut as firebaseSignOut } from "firebase/auth";
import { doc, getDoc, onSnapshot, updateDoc, arrayUnion, collection, query, where, getDocs, writeBatch } from "firebase/firestore";
import { ref, set, serverTimestamp } from 'firebase/database';
import { cryptoService } from '@/lib/crypto-service';
import { fingerprintService } from '@/lib/fingerprint-service';
import type { User } from '@/lib/types';
import { useFirebase } from '@/contexts/firebase-context';
import { Capacitor } from '@capacitor/core';
import { Device } from '@capacitor/device';
import { useToast } from '@/hooks/use-toast';
export const useAuthCore = () => {
const { auth, db, rtdb } = useFirebase();
const { toast } = useToast();
const addToast = useCallback((message: string, options?: { variant?: 'default' | 'destructive' }) => {
toast({ title: message, variant: options?.variant });
}, [toast]);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [authStatus, setAuthStatus] = useState<'loading' | 'onboarding' | 'recovery' | 'authenticated'>('loading');
const handleAuthenticatedUser = useCallback(async (user: FirebaseUser) => {
const userProfileRef = doc(db, 'users', user.uid);
const userSnap = await getDoc(userProfileRef);
if (userSnap.exists()) {
localStorage.setItem('userId', user.uid); // Save userId for Capacitor
await cryptoService.setupUserKeys(user.uid, rtdb);
const fingerprint = await fingerprintService.generate();
const existingData = userSnap.data();
if (fingerprint && (!existingData.deviceFingerprints || !existingData.deviceFingerprints.includes(fingerprint))) {
await updateDoc(userProfileRef, {
deviceFingerprints: arrayUnion(fingerprint)
});
}
const unsub = onSnapshot(userProfileRef, (doc) => {
if (doc.exists()) {
setCurrentUser({ uid: doc.id, ...doc.data() } as User);
} else {
setCurrentUser(null);
setAuthStatus('onboarding');
}
});
setAuthStatus('authenticated');
return unsub;
} else {
setAuthStatus('onboarding');
}
return () => {};
}, [db, rtdb]);
const recoverAccount = useCallback(async (recoveryId: string): Promise<boolean> => {
if (!auth.currentUser) {
addToast("Authentication not initialized. Please wait and try again.", { variant: "destructive" });
return false;
}
const newUid = auth.currentUser.uid;
// 1. Find original UID from recoveryId
const recoveryDocRef = doc(db, 'recovery', recoveryId);
const recoverySnap = await getDoc(recoveryDocRef);
if (!recoverySnap.exists()) {
addToast("Invalid Recovery ID.", { variant: "destructive" });
return false;
}
const originalUid = recoverySnap.data().uid;
if (originalUid === newUid) {
addToast("This account is already active on this device.", { variant: "default" });
await handleAuthenticatedUser(auth.currentUser);
return true;
}
addToast("Account found! Migrating to new device...");
try {
const batch = writeBatch(db);
const originalProfileRef = doc(db, 'users', originalUid);
const originalProfileSnap = await getDoc(originalProfileRef);
if (!originalProfileSnap.exists()) {
throw new Error("Original user profile not found. The account may be corrupted.");
}
const originalProfileData = originalProfileSnap.data() as User;
const publicId = originalProfileData.publicId;
const newProfileData = { ...originalProfileData, uid: newUid };
batch.set(doc(db, 'users', newUid), newProfileData);
batch.update(doc(db, 'publicIds', publicId), { uid: newUid });
batch.update(recoveryDocRef, { uid: newUid });
// To be safe, we are not deleting the old user document.
// This prevents data loss if the user is logged in on another device.
await batch.commit();
addToast("Account recovered successfully!");
await handleAuthenticatedUser(auth.currentUser);
return true;
} catch (error: any) {
console.error("Account migration failed:", error);
addToast(`Recovery failed: ${error.message}`, { variant: "destructive" });
return false;
}
}, [auth, db, addToast, handleAuthenticatedUser]);
useEffect(() => {
let isMounted = true;
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (!isMounted) return;
if (user) {
await handleAuthenticatedUser(user);
} else {
try {
await signInAnonymously(auth);
} catch (error) {
console.error("Anonymous sign-in failed:", error);
if (isMounted) setAuthStatus('recovery'); // Go to recovery if sign-in fails
}
}
});
return () => {
isMounted = false;
unsubscribe();
};
}, [auth, handleAuthenticatedUser]);
const signOutUser = useCallback(async () => {
const userToSignOut = auth.currentUser;
if (!userToSignOut && !currentUser) {
setAuthStatus('recovery');
window.location.reload();
return;
};
const uidToUpdate = currentUser?.uid || userToSignOut?.uid;
try {
if(uidToUpdate) {
const presenceRef = ref(rtdb, `presence/${uidToUpdate}`);
await set(presenceRef, { isOnline: false, lastSeen: serverTimestamp() });
}
await firebaseSignOut(auth);
} catch (error) {
console.error("Sign out error:", error);
} finally {
setCurrentUser(null);
setAuthStatus('recovery');
window.location.reload();
}
}, [auth, rtdb, currentUser]);
const updateUserProfile = useCallback(async (profileData: Partial<Pick<User, 'displayName' | 'photoURL' | 'bio' | 'status' | 'privacySettings'>>) => {
if (!currentUser) return;
const userRef = doc(db, 'users', currentUser.uid);
await updateDoc(userRef, profileData);
}, [currentUser, db]);
return {
currentUser,
authStatus,
setAuthStatus,
signOutUser,
recoverAccount,
updateUserProfile,
auth,
handleAuthenticatedUser,
};
};