// composables/useRecaptcha.ts (или use-recaptcha.ts для auto-import как useRecaptcha) import { useRuntimeConfig } from '#imports'; /** Управляет загрузкой reCAPTCHA v3 и получением токенов; загружает скрипт лениво при первом вызове initRecaptcha. */ export function useRecaptcha() { let recaptchaLoaded = false; let siteKey: string | null = null; async function loadRecaptchaScript(): Promise { // SSR-safe: только на клиенте if (import.meta.server) return; if (recaptchaLoaded) return; siteKey = useRuntimeConfig().public.recaptchaSiteKey; if (!siteKey) { console.warn('reCAPTCHA siteKey не задан — защита отключена'); return; } const useRecaptchaNet = true; // true = recaptcha.net (для регионов с блокировкой Google) const domain = useRecaptchaNet ? 'recaptcha.net' : 'www.google.com'; const script = document.createElement('script'); script.src = `https://${domain}/recaptcha/api.js?render=${siteKey}&hl=ru`; script.async = true; script.defer = true; document.head.appendChild(script); // Polling до готовности grecaptcha const maxAttempts = 100; // ~10 сек let attempts = 0; while (typeof (window as any).grecaptcha === 'undefined' || !(window as any).grecaptcha.execute) { if (attempts >= maxAttempts) { throw new Error('reCAPTCHA не загрузился (таймаут)'); } await new Promise(resolve => setTimeout(resolve, 100)); attempts++; } recaptchaLoaded = true; // Скрываем бейдж глобально (один раз) const style = document.createElement('style'); style.textContent = '.grecaptcha-badge { visibility: hidden !important; }'; document.head.appendChild(style); console.log('reCAPTCHA v3 загружен глобально'); } // Инициализация: вызывать в onMounted формы (даёт время на сбор данных о поведении) async function initRecaptcha(): Promise { if (import.meta.server || recaptchaLoaded || !siteKey) return; await loadRecaptchaScript(); } // Получение токена: вызывать в submit async function getRecaptchaToken(action: string): Promise { if (import.meta.server) { throw new Error('reCAPTCHA работает только на клиенте'); } if (!recaptchaLoaded) { await loadRecaptchaScript(); } if (!siteKey) { throw new Error('reCAPTCHA siteKey не задан'); } // @ts-expect-error grecaptcha глобальный return await grecaptcha.execute(siteKey, { action }); } return { initRecaptcha, getRecaptchaToken, }; }