fork(1) download
  1. import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
  2. import { auth, db } from '../lib/firebase';
  3. import {
  4. User,
  5. GoogleAuthProvider,
  6. OAuthProvider,
  7. PhoneAuthProvider,
  8. signInWithPopup,
  9. signInWithPhoneNumber,
  10. RecaptchaVerifier,
  11. ConfirmationResult
  12. } from 'firebase/auth';
  13. import { doc, getDoc, setDoc, Timestamp, collection } from 'firebase/firestore';
  14. import toast from 'react-hot-toast';
  15. import { useNavigate } from 'react-router-dom';
  16. import { TEST_USER_CONFIG, TEST_ENROLLMENTS } from './testUserSetup';
  17.  
  18. interface AuthContextType {
  19. user: User | null;
  20. loading: boolean;
  21. signInWithGoogle: () => Promise<void>;
  22. signInWithApple: () => Promise<void>;
  23. signInWithPhone: (phoneNumber: string) => Promise<string>;
  24. verifyOtp: (verificationId: string, otp: string) => Promise<void>;
  25. signOut: () => Promise<void>;
  26. }
  27.  
  28. declare global {
  29. interface Window {
  30. recaptchaVerifier: RecaptchaVerifier | null;
  31. recaptchaWidgetId: number | null;
  32. }
  33. }
  34.  
  35.  
  36. export function AuthProvider({ children }: { children: React.ReactNode }) {
  37. const [user, setUser] = useState<User | null>(null);
  38. const [loading, setLoading] = useState(true);
  39. const navigate = useNavigate();
  40. const confirmationResultRef = useRef<ConfirmationResult | null>(null);
  41.  
  42. const clearRecaptcha = () => {
  43. try {
  44. if (window.recaptchaVerifier) {
  45. window.recaptchaVerifier.clear();
  46. window.recaptchaVerifier = null;
  47. }
  48. const recaptchaElements = document.getElementsByClassName('grecaptcha-badge');
  49. while (recaptchaElements.length > 0) {
  50. recaptchaElements[0].remove();
  51. }
  52. const container = document.getElementById('recaptcha-container');
  53. if (container) {
  54. container.innerHTML = '';
  55. }
  56. window.recaptchaWidgetId = null;
  57. } catch (error) {
  58. console.error('Error clearing reCAPTCHA:', error);
  59. }
  60. };
  61.  
  62. const initializeUserData = async (userId: string, userData: any) => {
  63. try {
  64. const userDocRef = doc(db, 'users', userId);
  65. await setDoc(userDocRef, {
  66. uid: userId,
  67. displayName: userData.displayName || "New User",
  68. email: userData.email,
  69. phoneNumber: userData.phoneNumber,
  70. joinedDate: Timestamp.now(),
  71. totalFitCoins: userData.isTestUser ? 1000 : 0,
  72. activeCourses: 0,
  73. createdAt: Timestamp.now(),
  74. updatedAt: Timestamp.now(),
  75. lastLoginAt: Timestamp.now(),
  76. isTestUser: !!userData.isTestUser
  77. }, { merge: true });
  78. } catch (error) {
  79. console.error('Error initializing user data:', error);
  80. throw error;
  81. }
  82. };
  83.  
  84. useEffect(() => {
  85. const checkTestUser = async () => {
  86. const testUserData = localStorage.getItem('testUser');
  87. if (testUserData) {
  88. const parsedUser = JSON.parse(testUserData);
  89. setUser(parsedUser as unknown as User);
  90. setLoading(false);
  91. return;
  92. }
  93.  
  94. const unsubscribe = auth.onAuthStateChanged(async (user) => {
  95. if (user) {
  96. try {
  97. const userDocRef = doc(db, 'users', user.uid);
  98. const userDoc = await getDoc(userDocRef);
  99.  
  100. if (!userDoc.exists()) {
  101. await initializeUserData(user.uid, user);
  102. } else {
  103. await setDoc(userDocRef, {
  104. lastLoginAt: Timestamp.now(),
  105. updatedAt: Timestamp.now()
  106. }, { merge: true });
  107. }
  108. } catch (error) {
  109. console.error("Error handling user document:", error);
  110. }
  111. }
  112. setUser(user);
  113. setLoading(false);
  114. });
  115.  
  116. return () => {
  117. unsubscribe();
  118. clearRecaptcha();
  119. };
  120. };
  121.  
  122. checkTestUser();
  123. }, []);
  124.  
  125. const setupRecaptcha = () => {
  126. try {
  127. clearRecaptcha();
  128.  
  129. // Force cleanup of any existing reCAPTCHA elements
  130. document.querySelectorAll('.grecaptcha-badge').forEach(el => el.remove());
  131.  
  132. // Create or get container
  133. let container = document.getElementById('recaptcha-container');
  134. if (!container) {
  135. container = document.createElement('div');
  136. container.id = 'recaptcha-container';
  137. document.body.appendChild(container);
  138. }
  139. container.innerHTML = '';
  140.  
  141. const verifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
  142. size: 'invisible',
  143. callback: (response: any) => {
  144. console.log('reCAPTCHA verified:', response);
  145. },
  146. 'expired-callback': () => {
  147. console.log('reCAPTCHA expired');
  148. clearRecaptcha();
  149. toast.error('Security verification expired. Please try again.');
  150. },
  151. 'error-callback': (error: Error) => {
  152. console.error('reCAPTCHA error:', error);
  153. clearRecaptcha();
  154. toast.error('Security verification failed. Please refresh and try again.');
  155. }
  156. });
  157.  
  158. // Attempt to render immediately to check for errors
  159. return verifier;
  160. } catch (error) {
  161. console.error('Error setting up reCAPTCHA:', error);
  162. clearRecaptcha();
  163. throw new Error('Failed to set up security verification. Please refresh the page.');
  164. }
  165. };
  166.  
  167. const signInWithPhone = async (phoneNumber: string): Promise<string> => {
  168. try {
  169. const normalizedPhone = phoneNumber.startsWith('+91') ? phoneNumber : `+91${phoneNumber}`;
  170.  
  171. if (normalizedPhone === TEST_PHONE) {
  172. const testUserWithMethods = {
  173. ...TEST_USER,
  174. isTestUser: true
  175. };
  176. await setupTestUserData();
  177. localStorage.setItem('testUser', JSON.stringify(testUserWithMethods));
  178. setUser(testUserWithMethods as unknown as User);
  179. toast.success('Test account activated!');
  180. navigate('/profile');
  181. return 'test-verification-id';
  182. }
  183.  
  184.  
  185. // Clear any existing state
  186. clearRecaptcha();
  187.  
  188. // Set up new reCAPTCHA
  189. const verifier = setupRecaptcha();
  190.  
  191. // Explicitly render and wait for reCAPTCHA
  192. try {
  193. await verifier.render();
  194. } catch (error) {
  195. console.error('reCAPTCHA render error:', error);
  196. clearRecaptcha();
  197. throw new Error('Please refresh the page and try again');
  198. }
  199.  
  200. // Attempt phone authentication
  201. const confirmationResult = await signInWithPhoneNumber(auth, normalizedPhone, verifier);
  202.  
  203. if (!confirmationResult) {
  204. throw new Error('Failed to send verification code');
  205. }
  206.  
  207. // Store confirmation result
  208. confirmationResultRef.current = confirmationResult;
  209.  
  210. toast.success('Verification code sent!');
  211. return confirmationResult.verificationId;
  212. } catch (error: any) {
  213. clearRecaptcha();
  214. console.error('Phone sign-in error:', error);
  215.  
  216. // Handle specific error cases
  217. if (error.code === 'auth/invalid-app-credential' ||
  218. error.message?.includes('reCAPTCHA')) {
  219. throw new Error('Please refresh the page and try again');
  220. }
  221.  
  222. const errorMessages = {
  223. 'auth/invalid-phone-number': 'Invalid phone number format',
  224. 'auth/quota-exceeded': 'SMS quota exceeded. Please try again later',
  225. 'auth/captcha-check-failed': 'Security check failed. Please refresh and try again',
  226. 'auth/network-request-failed': 'Network error. Please check your connection',
  227. 'auth/too-many-requests': 'Too many attempts. Please try again later'
  228. } as const;
  229.  
  230. const message = errorMessages[error.code as keyof typeof errorMessages] ||
  231. error.message ||
  232. 'Failed to send verification code';
  233.  
  234. throw new Error(message);
  235. }
  236. };
  237.  
  238. const verifyOtp = async (verificationId: string, otp: string) => {
  239. try {
  240. if (verificationId === 'test-verification-id') {
  241. return;
  242. }
  243.  
  244. if (!confirmationResultRef.current) {
  245. throw new Error('Verification session expired. Please request a new code.');
  246. }
  247.  
  248. const result = await confirmationResultRef.current.confirm(otp);
  249.  
  250. if (!result.user) {
  251. throw new Error('Failed to verify code');
  252. }
  253.  
  254. await initializeUserData(result.user.uid, result.user);
  255. toast.success('Successfully signed in!');
  256. navigate('/profile');
  257. } catch (error: any) {
  258. console.error('OTP verification error:', error);
  259.  
  260. const errorMessages = {
  261. 'auth/invalid-verification-code': 'Invalid verification code',
  262. 'auth/code-expired': 'Verification code has expired',
  263. 'auth/invalid-verification-id': 'Session expired. Please request a new code',
  264. 'auth/network-request-failed': 'Network error. Please check your connection'
  265. } as const;
  266.  
  267. const message = errorMessages[error.code as keyof typeof errorMessages] ||
  268. 'Verification failed. Please try again.';
  269.  
  270. toast.error(message);
  271. throw error;
  272. } finally {
  273. clearRecaptcha();
  274. confirmationResultRef.current = null;
  275. }
  276. };
  277.  
  278. const value = {
  279. user,
  280. loading,
  281. signInWithGoogle,
  282. signInWithApple,
  283. signInWithPhone,
  284. verifyOtp,
  285. signOut,
  286. };
  287.  
  288. return (
  289. <AuthContext.Provider value={value}>
  290. {children}
  291. </AuthContext.Provider>
  292. );
  293. }
  294.  
Not running #stdin #stdout 0s 0KB
stdin
Standard input is empty
stdout
Standard output is empty