fix(auth): add dedicated GoogleAuthModal popup for reliable Google Sign-In

This commit is contained in:
Shanmuga Krishnan S M
2026-08-06 13:37:24 +05:30
parent 3ddcff8035
commit 1b698d7cca
2 changed files with 158 additions and 45 deletions

View File

@@ -0,0 +1,125 @@
import { useEffect, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, ShieldCheck, Sparkles } from 'lucide-react';
import { useAuth } from '@/context/AuthContext';
interface GoogleAuthModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProps) {
const buttonRef = useRef<HTMLDivElement>(null);
const { isAuthenticated } = useAuth();
// Close modal automatically if user gets authenticated
useEffect(() => {
if (isAuthenticated && isOpen) {
onClose();
}
}, [isAuthenticated, isOpen, onClose]);
// Render Google Button inside modal when open
useEffect(() => {
if (!isOpen) return;
const renderGoogleBtn = () => {
if (window.google && buttonRef.current) {
buttonRef.current.innerHTML = '';
window.google.accounts.id.renderButton(buttonRef.current, {
type: 'standard',
theme: 'outline',
size: 'large',
text: 'signin_with',
shape: 'pill',
logo_alignment: 'left',
width: 280,
});
}
};
// Render immediately if script is loaded
renderGoogleBtn();
// Or poll briefly if script is loading
const interval = setInterval(() => {
if (window.google) {
renderGoogleBtn();
clearInterval(interval);
}
}, 200);
return () => clearInterval(interval);
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-slate-950/70 backdrop-blur-md"
/>
{/* Modal Container */}
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
transition={{ type: 'spring', bounce: 0.2, duration: 0.3 }}
className="relative w-full max-w-md bg-slate-900 border border-slate-700/80 rounded-3xl shadow-2xl p-8 z-10 text-center overflow-hidden"
>
{/* Background Orbs */}
<div className="absolute -top-20 -right-20 w-40 h-40 bg-orange-500/20 rounded-full blur-2xl pointer-events-none" />
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-blue-500/20 rounded-full blur-2xl pointer-events-none" />
{/* Close Button */}
<button
onClick={onClose}
className="absolute top-4 right-4 p-2 rounded-full hover:bg-slate-800 text-slate-400 hover:text-white transition-colors cursor-pointer"
>
<X className="w-5 h-5" />
</button>
{/* Icon Header */}
<div className="w-16 h-16 rounded-2xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center mx-auto mb-5">
<Sparkles className="w-8 h-8 text-[#F97316]" />
</div>
{/* Title & Description */}
<h2
className="text-2xl font-bold text-white mb-2"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
Sign In to Freshers Hub
</h2>
<p
className="text-xs text-slate-300 mb-6 leading-relaxed"
style={{ fontFamily: 'Inter, sans-serif' }}
>
Sign in with your Google account. Use your{' '}
<strong className="text-[#F97316]">@ritchennai.edu.in</strong> college email to unlock the{' '}
<strong className="text-emerald-400">Verified Student</strong> badge and post collaboration requests!
</p>
{/* Google Sign-In Button Container */}
<div className="flex justify-center my-6 min-h-[44px]">
<div ref={buttonRef} />
</div>
{/* Verified Student Callout */}
<div className="mt-6 pt-4 border-t border-slate-800/80 flex items-center justify-center gap-2 text-[11px] text-slate-400">
<ShieldCheck className="w-4 h-4 text-emerald-400 shrink-0" />
<span>Official RIT Student Verification via Google</span>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}

View File

@@ -1,6 +1,7 @@
import { createContext, useContext, useState, useEffect, useCallback } from 'react'; import { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { getBackendUrl } from '@/lib/utils'; import { getBackendUrl } from '@/lib/utils';
import GoogleAuthModal from '@/components/Auth/GoogleAuthModal';
// ────────────────────────────────────────────────── // ──────────────────────────────────────────────────
// Types // Types
@@ -19,7 +20,10 @@ interface AuthContextType {
isAuthenticated: boolean; isAuthenticated: boolean;
isVerifiedStudent: boolean; isVerifiedStudent: boolean;
isLoading: boolean; isLoading: boolean;
isAuthModalOpen: boolean;
loginWithGoogle: () => void; loginWithGoogle: () => void;
openAuthModal: () => void;
closeAuthModal: () => void;
logout: () => void; logout: () => void;
} }
@@ -27,10 +31,9 @@ interface AuthContextType {
// Google Client ID // Google Client ID
// ────────────────────────────────────────────────── // ──────────────────────────────────────────────────
// IMPORTANT: Replace this with your actual Google Cloud Console OAuth2 Client ID const GOOGLE_CLIENT_ID =
// Create one at: https://console.cloud.google.com/apis/credentials import.meta.env.VITE_GOOGLE_CLIENT_ID ||
// Authorized origins: https://rit-services.in, http://localhost:5173 '81935488244-vmab5t8fipfof1n6l7di7eg4fve6k8lf.apps.googleusercontent.com';
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID || '';
// ────────────────────────────────────────────────── // ──────────────────────────────────────────────────
// Context // Context
@@ -41,7 +44,10 @@ const AuthContext = createContext<AuthContextType>({
isAuthenticated: false, isAuthenticated: false,
isVerifiedStudent: false, isVerifiedStudent: false,
isLoading: true, isLoading: true,
isAuthModalOpen: false,
loginWithGoogle: () => {}, loginWithGoogle: () => {},
openAuthModal: () => {},
closeAuthModal: () => {},
logout: () => {}, logout: () => {},
}); });
@@ -75,6 +81,10 @@ const STORAGE_KEY = 'rit_auth_user';
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null); const [user, setUser] = useState<AuthUser | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
const openAuthModal = useCallback(() => setIsAuthModalOpen(true), []);
const closeAuthModal = useCallback(() => setIsAuthModalOpen(false), []);
// Handle the credential response from Google GSI // Handle the credential response from Google GSI
const handleCredentialResponse = useCallback(async (response: { credential: string }) => { const handleCredentialResponse = useCallback(async (response: { credential: string }) => {
@@ -93,6 +103,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const userData: AuthUser = await res.json(); const userData: AuthUser = await res.json();
setUser(userData); setUser(userData);
localStorage.setItem(STORAGE_KEY, JSON.stringify(userData)); localStorage.setItem(STORAGE_KEY, JSON.stringify(userData));
setIsAuthModalOpen(false);
} catch (err) { } catch (err) {
console.error('Google auth error:', err); console.error('Google auth error:', err);
} }
@@ -109,17 +120,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// Validate session is still valid against backend // Validate session is still valid against backend
fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`)) fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`))
.then(res => { .then((res) => {
if (!res.ok) { if (!res.ok) {
// Session expired, clear it
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
setUser(null); setUser(null);
} else { } else {
// Update with fresh data
return res.json(); return res.json();
} }
}) })
.then(freshData => { .then((freshData) => {
if (freshData) { if (freshData) {
setUser(freshData); setUser(freshData);
localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData)); localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData));
@@ -146,7 +155,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}); });
}; };
// The GSI script may not have loaded yet (async), so poll briefly
if (window.google) { if (window.google) {
initGSI(); initGSI();
} else { } else {
@@ -156,57 +164,33 @@ export function AuthProvider({ children }: { children: ReactNode }) {
initGSI(); initGSI();
} }
}, 200); }, 200);
// Stop polling after 10 seconds
setTimeout(() => clearInterval(interval), 10000); setTimeout(() => clearInterval(interval), 10000);
} }
}, [handleCredentialResponse]); }, [handleCredentialResponse]);
const loginWithGoogle = useCallback(() => { const loginWithGoogle = useCallback(() => {
if (!GOOGLE_CLIENT_ID) { // Open the clean Google Sign-In modal directly
console.warn('Google Client ID not configured. Set VITE_GOOGLE_CLIENT_ID in .env'); setIsAuthModalOpen(true);
return;
}
// Also trigger One Tap prompt as a secondary background hint if supported
if (window.google) { if (window.google) {
// Trigger the One Tap prompt try {
window.google.accounts.id.prompt((notification) => { window.google.accounts.id.prompt();
if (notification.isNotDisplayed() || notification.isSkippedMoment()) { } catch {
// If One Tap doesn't show (e.g. blocked by browser), fall back to button-based flow // Silently ignore if browser blocks One Tap
// Render a temporary popup button
const popup = document.createElement('div');
popup.id = 'google-signin-popup';
popup.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:99999;background:white;padding:32px;border-radius:16px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);';
document.body.appendChild(popup);
window.google!.accounts.id.renderButton(popup, {
theme: 'outline',
size: 'large',
text: 'signin_with',
shape: 'pill',
width: 300,
});
// Remove after 30s or when clicked
setTimeout(() => popup.remove(), 30000);
const observer = new MutationObserver(() => {
if (!document.getElementById('google-signin-popup')) {
observer.disconnect();
} }
});
observer.observe(document.body, { childList: true });
}
});
} }
}, []); }, []);
const logout = useCallback(() => { const logout = useCallback(() => {
if (user?.email && window.google) { if (user?.email && window.google) {
window.google.accounts.id.revoke(user.email, () => { try {
console.log('Google session revoked'); window.google.accounts.id.revoke(user.email, () => {});
}); } catch {}
} }
setUser(null); setUser(null);
localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY);
setIsAuthModalOpen(false);
}, [user]); }, [user]);
return ( return (
@@ -216,11 +200,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
isAuthenticated: !!user, isAuthenticated: !!user,
isVerifiedStudent: !!user?.verifiedStudent, isVerifiedStudent: !!user?.verifiedStudent,
isLoading, isLoading,
isAuthModalOpen,
loginWithGoogle, loginWithGoogle,
openAuthModal,
closeAuthModal,
logout, logout,
}} }}
> >
{children} {children}
<GoogleAuthModal isOpen={isAuthModalOpen} onClose={closeAuthModal} />
</AuthContext.Provider> </AuthContext.Provider>
); );
} }