Files
Spanglish/frontend/src/components/GoogleSignInButton.tsx
T
MichilisandCursor a6840ea953 Harden auth, payments, and frontend against review findings.
Close exploitable gaps in booking/payment flows, enforce token versioning and account checks, gate sensitive payment data, and add middleware plus input validation across admin routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 19:59:02 +00:00

217 lines
6.3 KiB
TypeScript

'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
import { useAuth } from '@/context/AuthContext';
import { useLanguage } from '@/context/LanguageContext';
import { safeInternalPath } from '@/lib/safeRedirect';
import toast from 'react-hot-toast';
declare global {
interface Window {
google?: {
accounts: {
id: {
initialize: (config: GoogleInitConfig) => void;
renderButton: (element: HTMLElement | null, options: GoogleButtonOptions) => void;
prompt: () => void;
cancel: () => void;
};
};
};
}
}
interface GoogleInitConfig {
client_id: string;
callback: (response: GoogleCredentialResponse) => void;
auto_select?: boolean;
cancel_on_tap_outside?: boolean;
}
interface GoogleButtonOptions {
type?: 'standard' | 'icon';
theme?: 'outline' | 'filled_blue' | 'filled_black';
size?: 'large' | 'medium' | 'small';
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';
shape?: 'rectangular' | 'pill' | 'circle' | 'square';
logo_alignment?: 'left' | 'center';
width?: string | number;
locale?: string;
}
interface GoogleCredentialResponse {
credential: string;
select_by?: string;
}
interface GoogleSignInButtonProps {
onSuccess?: () => void;
onError?: (error: string) => void;
redirectTo?: string;
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin';
className?: string;
}
export default function GoogleSignInButton({
onSuccess,
onError,
redirectTo = '/dashboard',
text = 'continue_with',
className = '',
}: GoogleSignInButtonProps) {
const buttonRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [scriptLoaded, setScriptLoaded] = useState(false);
const [scriptError, setScriptError] = useState(false);
const { loginWithGoogle } = useAuth();
const { locale } = useLanguage();
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
const handleGoogleCallback = useCallback(
async (response: GoogleCredentialResponse) => {
if (!response.credential) {
const errorMsg = locale === 'es' ? 'No se recibio credencial de Google' : 'No credential received from Google';
onError?.(errorMsg);
toast.error(errorMsg);
return;
}
setIsLoading(true);
try {
await loginWithGoogle(response.credential);
toast.success(locale === 'es' ? 'Bienvenido!' : 'Welcome!');
onSuccess?.();
// Use window.location for navigation to ensure clean state.
// Constrain to a same-origin path to avoid open redirects.
window.location.href = safeInternalPath(redirectTo, '/dashboard');
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Google login failed';
const displayError = locale === 'es' ? 'Error al iniciar sesion con Google' : errorMessage;
onError?.(displayError);
toast.error(displayError);
} finally {
setIsLoading(false);
}
},
[loginWithGoogle, locale, onSuccess, onError, redirectTo]
);
const initializeGoogleSignIn = useCallback(() => {
if (!clientId) {
console.warn('Google Client ID not configured');
return;
}
if (!window.google?.accounts?.id) {
return;
}
try {
window.google.accounts.id.initialize({
client_id: clientId,
callback: handleGoogleCallback,
auto_select: false,
cancel_on_tap_outside: true,
});
if (buttonRef.current) {
// Clear any existing button
buttonRef.current.innerHTML = '';
window.google.accounts.id.renderButton(buttonRef.current, {
type: 'standard',
theme: 'outline',
size: 'large',
text: text,
shape: 'rectangular',
logo_alignment: 'left',
width: 280,
locale: locale === 'es' ? 'es' : 'en',
});
}
} catch (error) {
console.error('Error initializing Google Sign-In:', error);
setScriptError(true);
}
}, [clientId, handleGoogleCallback, text, locale]);
// Load Google Sign-In script
useEffect(() => {
if (!clientId) {
return;
}
// Check if script is already loaded
if (window.google?.accounts?.id) {
setScriptLoaded(true);
return;
}
// Check if script tag already exists
const existingScript = document.querySelector('script[src="https://accounts.google.com/gsi/client"]');
if (existingScript) {
// Script exists but may not be loaded yet
existingScript.addEventListener('load', () => setScriptLoaded(true));
existingScript.addEventListener('error', () => setScriptError(true));
return;
}
// Load the script
const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
script.defer = true;
script.onload = () => setScriptLoaded(true);
script.onerror = () => setScriptError(true);
document.head.appendChild(script);
return () => {
// Cleanup is handled by checking for existing script
};
}, [clientId]);
// Initialize when script is loaded
useEffect(() => {
if (scriptLoaded) {
// Small delay to ensure Google object is fully available
const timer = setTimeout(initializeGoogleSignIn, 100);
return () => clearTimeout(timer);
}
}, [scriptLoaded, initializeGoogleSignIn]);
// Don't render if no client ID configured
if (!clientId) {
return null;
}
if (scriptError) {
return (
<div className={`text-center text-sm text-gray-500 py-2 ${className}`}>
{locale === 'es'
? 'Google Sign-In no disponible'
: 'Google Sign-In unavailable'}
</div>
);
}
return (
<div className={`relative ${className}`}>
{/* Google Sign-In Button Container */}
<div
ref={buttonRef}
className="flex justify-center min-h-[44px]"
aria-label={locale === 'es' ? 'Iniciar sesion con Google' : 'Sign in with Google'}
/>
{/* Loading overlay */}
{isLoading && (
<div className="absolute inset-0 bg-white/80 flex items-center justify-center rounded">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-secondary-blue" />
</div>
)}
</div>
);
}