Files
Hotel-Booking/Frontend/src/shared/components/Recaptcha.tsx
Iliyan Angelov 39fcfff811 update
2025-11-30 22:43:09 +02:00

176 lines
4.1 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import ReCAPTCHA from 'react-google-recaptcha';
import { recaptchaService } from '../../features/system/services/systemSettingsService';
interface RecaptchaProps {
onChange?: (token: string | null) => void;
onError?: (error: string) => void;
theme?: 'light' | 'dark';
size?: 'normal' | 'compact';
className?: string;
}
// Cache for reCAPTCHA settings to avoid multiple API calls
interface RecaptchaSettingsCache {
siteKey: string;
enabled: boolean;
timestamp: number;
}
const CACHE_KEY = 'recaptcha_settings_cache';
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
let settingsCache: RecaptchaSettingsCache | null = null;
let fetchPromise: Promise<RecaptchaSettingsCache | null> | null = null;
const getCachedSettings = (): RecaptchaSettingsCache | null => {
// Check in-memory cache first
if (settingsCache) {
const age = Date.now() - settingsCache.timestamp;
if (age < CACHE_DURATION) {
return settingsCache;
}
}
// Check localStorage cache
try {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const parsed: RecaptchaSettingsCache = JSON.parse(cached);
const age = Date.now() - parsed.timestamp;
if (age < CACHE_DURATION) {
settingsCache = parsed;
return parsed;
}
}
} catch (error) {
// Ignore cache errors
}
return null;
};
const fetchRecaptchaSettings = async (): Promise<RecaptchaSettingsCache | null> => {
// If there's already a fetch in progress, return that promise
if (fetchPromise) {
return fetchPromise;
}
fetchPromise = (async () => {
try {
const response = await recaptchaService.getRecaptchaSettings();
if (response.status === 'success' && response.data) {
const settings: RecaptchaSettingsCache = {
siteKey: response.data.recaptcha_site_key || '',
enabled: response.data.recaptcha_enabled || false,
timestamp: Date.now(),
};
// Update caches
settingsCache = settings;
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(settings));
} catch (error) {
// Ignore localStorage errors
}
return settings;
}
return null;
} catch (error) {
console.error('Error fetching reCAPTCHA settings:', error);
return null;
} finally {
fetchPromise = null;
}
})();
return fetchPromise;
};
const Recaptcha: React.FC<RecaptchaProps> = ({
onChange,
onError,
theme = 'dark',
size = 'normal',
className = '',
}) => {
const recaptchaRef = useRef<ReCAPTCHA>(null);
const [siteKey, setSiteKey] = useState<string>('');
const [enabled, setEnabled] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const loadSettings = async () => {
// Try to get from cache first
const cached = getCachedSettings();
if (cached) {
setSiteKey(cached.siteKey);
setEnabled(cached.enabled);
setLoading(false);
return;
}
// Fetch from API if not cached
const settings = await fetchRecaptchaSettings();
if (settings) {
setSiteKey(settings.siteKey);
setEnabled(settings.enabled);
} else {
if (onError) {
onError('Failed to load reCAPTCHA settings');
}
}
setLoading(false);
};
loadSettings();
}, [onError]);
const handleChange = (token: string | null) => {
if (onChange) {
onChange(token);
}
};
const handleExpired = () => {
if (onChange) {
onChange(null);
}
};
const handleError = () => {
if (onError) {
onError('reCAPTCHA error occurred');
}
if (onChange) {
onChange(null);
}
};
if (loading) {
return null;
}
if (!enabled || !siteKey) {
return null;
}
return (
<div className={className}>
<ReCAPTCHA
ref={recaptchaRef}
sitekey={siteKey}
onChange={handleChange}
onExpired={handleExpired}
onError={handleError}
theme={theme}
size={size}
/>
</div>
);
};
export default Recaptcha;