/** * Hook برای ارتباط دو طرفه با Flutter WebView * دریافت و ارسال ایونت‌ها */ import { useEffect, useRef, useState } from 'react'; // تایپ‌های ایونت‌های دریافتی از Flutter export type FlutterEventType = | 'LAYOUT_CHANGED' | 'KEYBOARD_CHANGED' | 'SAFE_AREA_CHANGED' | 'ORIENTATION_CHANGED' | 'SCREEN_SIZE_CHANGED' | 'INITIAL_CONFIG'; export interface FlutterEvent { type: FlutterEventType; payload: any; timestamp?: number; } // تایپ‌های ایونت‌های ارسالی به Flutter export type WebEventType = | 'WEB_READY' | 'REQUEST_LOCATION' | 'REQUEST_CONSULTANT' | 'PAGE_LOADED' | 'ERROR_OCCURRED'; export interface WebEvent { type: WebEventType; payload?: any; timestamp: number; } interface UseFlutterBridgeOptions { onEvent?: (event: FlutterEvent) => void; enableLogging?: boolean; } interface UseFlutterBridgeReturn { // دریافت ایونت‌ها events: FlutterEvent[]; lastEvent: FlutterEvent | null; // ارسال ایونت‌ها sendToFlutter: (type: WebEventType, payload?: any) => void; // آماده بودن isReady: boolean; // لاگ‌ها logs: string[]; } /** * نگاشت ایونت‌های وب به اکشن‌های واقعی Flutter. * اپ Flutter (najm) فقط کانال `HabibApp` را ثبت می‌کند و پیام‌هایی با * فرمت `{ action, data }` را می‌فهمد. کانال `FlutterChannel` در Flutter * وجود ندارد، بنابراین تنها مسیر معتبر ارسال، همین کانال است. */ const WEB_EVENT_TO_ACTION: Record = { WEB_READY: "web_ready", REQUEST_LOCATION: "get_location", REQUEST_CONSULTANT: "open_consultant_page", PAGE_LOADED: "page_loaded", ERROR_OCCURRED: "error_occurred", }; export function useFlutterBridge( options: UseFlutterBridgeOptions = {} ): UseFlutterBridgeReturn { const { onEvent, enableLogging = true } = options; const [events, setEvents] = useState([]); const [lastEvent, setLastEvent] = useState(null); const [isReady, setIsReady] = useState(false); const [logs, setLogs] = useState([]); const logsRef = useRef([]); const hasSentReadyRef = useRef(false); // تابع کمکی برای اضافه کردن لاگ const addLog = (message: string, type: 'info' | 'success' | 'error' = 'info') => { const timestamp = new Date().toLocaleTimeString('fa-IR'); const logMessage = `[${timestamp}] [${type.toUpperCase()}] ${message}`; if (enableLogging) { console.log(logMessage); } logsRef.current = [...logsRef.current, logMessage].slice(-50); // نگه داشتن 50 لاگ آخر setLogs(logsRef.current); }; // تابع ارسال ایونت به Flutter const sendToFlutter = (type: WebEventType, payload?: any) => { if (typeof window === 'undefined') return; const action = WEB_EVENT_TO_ACTION[type]; let delivered = false; try { // روش اصلی و واقعی: کانال HabibApp با فرمت { action, data } // این تنها کانالی است که اپ Flutter ثبت کرده و به آن گوش می‌دهد. if (window.HabibApp?.postMessage) { const message = payload !== undefined ? { action, data: payload } : { action }; window.HabibApp.postMessage(JSON.stringify(message)); delivered = true; addLog(`📤 ارسال به Flutter (HabibApp): ${type} → ${action}`, 'success'); } // مسیرهای fallback فقط برای محیط توسعه/iframe (در WebView واقعی Flutter وجود ندارند) const event: WebEvent = { type, payload, timestamp: Date.now() }; if (window.parent && window.parent !== window) { window.parent.postMessage(event, '*'); delivered = true; } if ((window as any).FlutterChannel) { (window as any).FlutterChannel.postMessage(JSON.stringify(event)); delivered = true; } if ((window as any).webkit?.messageHandlers?.FlutterChannel) { (window as any).webkit.messageHandlers.FlutterChannel.postMessage(event); delivered = true; } if (!delivered) { addLog(`⚠️ محیط Flutter یافت نشد - در حالت توسعه`, 'info'); } } catch (error) { addLog(`❌ خطا در ارسال به Flutter: ${error}`, 'error'); console.error('Error sending to Flutter:', error); } }; // Listen کردن به ایونت‌های Flutter useEffect(() => { addLog('🚀 شروع listening به ایونت‌های Flutter', 'info'); const handleFlutterEvent = (event: MessageEvent) => { try { const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; // چک کردن اینکه ایونت از Flutter است if (data && data.type) { const flutterEvent: FlutterEvent = { type: data.type, payload: data.payload, timestamp: data.timestamp || Date.now(), }; addLog(`📥 دریافت از Flutter: ${data.type}`, 'success'); setEvents((prev) => [...prev, flutterEvent].slice(-20)); // نگه داشتن 20 ایونت آخر setLastEvent(flutterEvent); // Handle INITIAL_CONFIG if (data.type === 'INITIAL_CONFIG') { setIsReady(true); addLog('✅ WebView آماده شد', 'success'); } // Callback if (onEvent) { onEvent(flutterEvent); } } } catch (error) { addLog(`❌ خطا در parse ایونت Flutter: ${error}`, 'error'); console.error('Error parsing Flutter event:', error); } }; // Listen به message event window.addEventListener('message', handleFlutterEvent); // Listen به custom events const handleCustomEvent = (event: Event) => { const customEvent = event as CustomEvent; if (customEvent.detail) { handleFlutterEvent({ data: customEvent.detail, } as MessageEvent); } }; window.addEventListener('flutterConfig', handleCustomEvent as EventListener); window.addEventListener('flutterEvent', handleCustomEvent as EventListener); // Cleanup return () => { window.removeEventListener('message', handleFlutterEvent); window.removeEventListener('flutterConfig', handleCustomEvent as EventListener); window.removeEventListener('flutterEvent', handleCustomEvent as EventListener); addLog('🛑 متوقف کردن listening', 'info'); }; }, [onEvent, enableLogging]); // تشخیص آمادگی واقعی + ارسال خودکار WEB_READY // در WebView واقعی Flutter، شیء window.HabibApp توسط addJavaScriptChannel // تزریق می‌شود؛ وجودش یعنی پل برقرار است. منتظر INITIAL_CONFIG نمی‌مانیم، // چون Flutter چنین ایونتی نمی‌فرستد. useEffect(() => { if (typeof window === 'undefined') return; let interval: ReturnType | undefined; let timeout: ReturnType | undefined; const markReadyAndAnnounce = () => { if (!window.HabibApp?.postMessage) return false; setIsReady(true); if (!hasSentReadyRef.current) { hasSentReadyRef.current = true; sendToFlutter('WEB_READY', { url: window.location.href, userAgent: navigator.userAgent, timestamp: Date.now(), }); addLog('✅ WEB_READY به‌صورت خودکار ارسال شد', 'success'); } return true; }; // اگر کانال هنوز تزریق نشده، کمی صبر می‌کنیم (تزریق ممکن است با تأخیر باشد) if (!markReadyAndAnnounce()) { interval = setInterval(() => { if (markReadyAndAnnounce() && interval) clearInterval(interval); }, 100); timeout = setTimeout(() => { if (interval) clearInterval(interval); }, 5000); } return () => { if (interval) clearInterval(interval); if (timeout) clearTimeout(timeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // گوش‌دادن به پاسخ‌های واقعی Flutter از مسیر window.onFlutterResponse. // رسیدن هر پاسخ یعنی ارتباط دوطرفه برقرار است. useEffect(() => { if (typeof window === 'undefined') return; const unsubscribe = window.addFlutterResponseListener?.((response) => { setIsReady(true); const flutterEvent: FlutterEvent = { type: 'INITIAL_CONFIG', payload: response, timestamp: response?.data?.timestamp || Date.now(), }; addLog(`📥 پاسخ از Flutter: ${response?.action}`, 'success'); setEvents((prev) => [...prev, flutterEvent].slice(-20)); setLastEvent(flutterEvent); onEvent?.(flutterEvent); }); return () => { unsubscribe?.(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [onEvent]); return { events, lastEvent, sendToFlutter, isReady, logs, }; }