176 lines
4.1 KiB
TypeScript
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;
|
|
|