/** * 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 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]); // Detect HabibApp bridge availability and set isReady state. // web_ready delivery is owned exclusively by the root bootstrap script // in layout.tsx — this hook must NOT independently send web_ready. useEffect(() => { if (typeof window === "undefined") return; if (window.HabibApp?.postMessage) { setIsReady(true); return; } let interval: ReturnType | undefined; let timeout: ReturnType | undefined; interval = setInterval(() => { if (window.HabibApp?.postMessage) { setIsReady(true); if (interval) clearInterval(interval); } }, 100); timeout = setTimeout(() => { if (interval) clearInterval(interval); }, 5000); return () => { if (interval) clearInterval(interval); if (timeout) clearTimeout(timeout); }; }, []); // گوش‌دادن به پاسخ‌های واقعی 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, }; }