import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import { auth, db } from '../lib/firebase';
import {
User,
GoogleAuthProvider,
OAuthProvider,
PhoneAuthProvider,
signInWithPopup,
signInWithPhoneNumber,
RecaptchaVerifier,
ConfirmationResult
} from 'firebase/auth';
import { doc, getDoc, setDoc, Timestamp, collection } from 'firebase/firestore';
import toast from 'react-hot-toast';
import { useNavigate } from 'react-router-dom';
import { TEST_USER_CONFIG, TEST_ENROLLMENTS } from './testUserSetup';
interface AuthContextType {
user: User | null;
loading: boolean;
signInWithGoogle: () => Promise<void>;
signInWithApple: () => Promise<void>;
signInWithPhone: (phoneNumber: string) => Promise<string>;
verifyOtp: (verificationId: string, otp: string) => Promise<void>;
signOut: () => Promise<void>;
}
declare global {
interface Window {
recaptchaVerifier: RecaptchaVerifier | null;
recaptchaWidgetId: number | null;
}
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const confirmationResultRef = useRef<ConfirmationResult | null>(null);
const clearRecaptcha = () => {
try {
if (window.recaptchaVerifier) {
window.recaptchaVerifier.clear();
window.recaptchaVerifier = null;
}
const recaptchaElements = document.getElementsByClassName('grecaptcha-badge');
while (recaptchaElements.length > 0) {
recaptchaElements[0].remove();
}
const container = document.getElementById('recaptcha-container');
if (container) {
container.innerHTML = '';
}
window.recaptchaWidgetId = null;
} catch (error) {
console.error('Error clearing reCAPTCHA:', error);
}
};
const initializeUserData = async (userId: string, userData: any) => {
try {
const userDocRef = doc(db, 'users', userId);
await setDoc(userDocRef, {
uid: userId,
displayName: userData.displayName || "New User",
email: userData.email,
phoneNumber: userData.phoneNumber,
joinedDate: Timestamp.now(),
totalFitCoins: userData.isTestUser ? 1000 : 0,
activeCourses: 0,
createdAt: Timestamp.now(),
updatedAt: Timestamp.now(),
lastLoginAt: Timestamp.now(),
isTestUser: !!userData.isTestUser
}, { merge: true });
} catch (error) {
console.error('Error initializing user data:', error);
throw error;
}
};
useEffect(() => {
const checkTestUser = async () => {
const testUserData = localStorage.getItem('testUser');
if (testUserData) {
const parsedUser = JSON.parse(testUserData);
setUser(parsedUser as unknown as User);
setLoading(false);
return;
}
const unsubscribe = auth.onAuthStateChanged(async (user) => {
if (user) {
try {
const userDocRef = doc(db, 'users', user.uid);
const userDoc = await getDoc(userDocRef);
if (!userDoc.exists()) {
await initializeUserData(user.uid, user);
} else {
await setDoc(userDocRef, {
lastLoginAt: Timestamp.now(),
updatedAt: Timestamp.now()
}, { merge: true });
}
} catch (error) {
console.error("Error handling user document:", error);
}
}
setUser(user);
setLoading(false);
});
return () => {
unsubscribe();
clearRecaptcha();
};
};
checkTestUser();
}, []);
const setupRecaptcha = () => {
try {
clearRecaptcha();
// Force cleanup of any existing reCAPTCHA elements
document.querySelectorAll('.grecaptcha-badge').forEach(el => el.remove());
// Create or get container
let container = document.getElementById('recaptcha-container');
if (!container) {
container = document.createElement('div');
container.id = 'recaptcha-container';
document.body.appendChild(container);
}
container.innerHTML = '';
const verifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
size: 'invisible',
callback: (response: any) => {
console.log('reCAPTCHA verified:', response);
},
'expired-callback': () => {
console.log('reCAPTCHA expired');
clearRecaptcha();
toast.error('Security verification expired. Please try again.');
},
'error-callback': (error: Error) => {
console.error('reCAPTCHA error:', error);
clearRecaptcha();
toast.error('Security verification failed. Please refresh and try again.');
}
});
// Attempt to render immediately to check for errors
return verifier;
} catch (error) {
console.error('Error setting up reCAPTCHA:', error);
clearRecaptcha();
throw new Error('Failed to set up security verification. Please refresh the page.');
}
};
const signInWithPhone = async (phoneNumber: string): Promise<string> => {
try {
const normalizedPhone = phoneNumber.startsWith('+91') ? phoneNumber : `+91${phoneNumber}`;
if (normalizedPhone === TEST_PHONE) {
const testUserWithMethods = {
...TEST_USER,
isTestUser: true
};
await setupTestUserData();
localStorage.setItem('testUser', JSON.stringify(testUserWithMethods));
setUser(testUserWithMethods as unknown as User);
toast.success('Test account activated!');
navigate('/profile');
return 'test-verification-id';
}
// Clear any existing state
clearRecaptcha();
// Set up new reCAPTCHA
const verifier = setupRecaptcha();
// Explicitly render and wait for reCAPTCHA
try {
await verifier.render();
} catch (error) {
console.error('reCAPTCHA render error:', error);
clearRecaptcha();
throw new Error('Please refresh the page and try again');
}
// Attempt phone authentication
const confirmationResult = await signInWithPhoneNumber(auth, normalizedPhone, verifier);
if (!confirmationResult) {
throw new Error('Failed to send verification code');
}
// Store confirmation result
confirmationResultRef.current = confirmationResult;
toast.success('Verification code sent!');
return confirmationResult.verificationId;
} catch (error: any) {
clearRecaptcha();
console.error('Phone sign-in error:', error);
// Handle specific error cases
if (error.code === 'auth/invalid-app-credential' ||
error.message?.includes('reCAPTCHA')) {
throw new Error('Please refresh the page and try again');
}
const errorMessages = {
'auth/invalid-phone-number': 'Invalid phone number format',
'auth/quota-exceeded': 'SMS quota exceeded. Please try again later',
'auth/captcha-check-failed': 'Security check failed. Please refresh and try again',
'auth/network-request-failed': 'Network error. Please check your connection',
'auth/too-many-requests': 'Too many attempts. Please try again later'
} as const;
const message = errorMessages[error.code as keyof typeof errorMessages] ||
error.message ||
'Failed to send verification code';
throw new Error(message);
}
};
const verifyOtp = async (verificationId: string, otp: string) => {
try {
if (verificationId === 'test-verification-id') {
return;
}
if (!confirmationResultRef.current) {
throw new Error('Verification session expired. Please request a new code.');
}
const result = await confirmationResultRef.current.confirm(otp);
if (!result.user) {
throw new Error('Failed to verify code');
}
await initializeUserData(result.user.uid, result.user);
toast.success('Successfully signed in!');
navigate('/profile');
} catch (error: any) {
console.error('OTP verification error:', error);
const errorMessages = {
'auth/invalid-verification-code': 'Invalid verification code',
'auth/code-expired': 'Verification code has expired',
'auth/invalid-verification-id': 'Session expired. Please request a new code',
'auth/network-request-failed': 'Network error. Please check your connection'
} as const;
const message = errorMessages[error.code as keyof typeof errorMessages] ||
'Verification failed. Please try again.';
toast.error(message);
throw error;
} finally {
clearRecaptcha();
confirmationResultRef.current = null;
}
};
const value = {
user,
loading,
signInWithGoogle,
signInWithApple,
signInWithPhone,
verifyOtp,
signOut,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}