|
|
@ -54,6 +54,20 @@ interface UseFlutterBridgeReturn { |
|
|
logs: string[]; |
|
|
logs: string[]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* نگاشت ایونتهای وب به اکشنهای واقعی Flutter. |
|
|
|
|
|
* اپ Flutter (najm) فقط کانال `HabibApp` را ثبت میکند و پیامهایی با |
|
|
|
|
|
* فرمت `{ action, data }` را میفهمد. کانال `FlutterChannel` در Flutter |
|
|
|
|
|
* وجود ندارد، بنابراین تنها مسیر معتبر ارسال، همین کانال است. |
|
|
|
|
|
*/ |
|
|
|
|
|
const WEB_EVENT_TO_ACTION: Record<WebEventType, string> = { |
|
|
|
|
|
WEB_READY: "web_ready", |
|
|
|
|
|
REQUEST_LOCATION: "get_location", |
|
|
|
|
|
REQUEST_CONSULTANT: "open_consultant_page", |
|
|
|
|
|
PAGE_LOADED: "page_loaded", |
|
|
|
|
|
ERROR_OCCURRED: "error_occurred", |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
export function useFlutterBridge( |
|
|
export function useFlutterBridge( |
|
|
options: UseFlutterBridgeOptions = {} |
|
|
options: UseFlutterBridgeOptions = {} |
|
|
): UseFlutterBridgeReturn { |
|
|
): UseFlutterBridgeReturn { |
|
|
@ -65,6 +79,7 @@ export function useFlutterBridge( |
|
|
const [logs, setLogs] = useState<string[]>([]); |
|
|
const [logs, setLogs] = useState<string[]>([]); |
|
|
|
|
|
|
|
|
const logsRef = useRef<string[]>([]); |
|
|
const logsRef = useRef<string[]>([]); |
|
|
|
|
|
const hasSentReadyRef = useRef(false); |
|
|
|
|
|
|
|
|
// تابع کمکی برای اضافه کردن لاگ
|
|
|
// تابع کمکی برای اضافه کردن لاگ
|
|
|
const addLog = (message: string, type: 'info' | 'success' | 'error' = 'info') => { |
|
|
const addLog = (message: string, type: 'info' | 'success' | 'error' = 'info') => { |
|
|
@ -81,37 +96,41 @@ export function useFlutterBridge( |
|
|
|
|
|
|
|
|
// تابع ارسال ایونت به Flutter
|
|
|
// تابع ارسال ایونت به Flutter
|
|
|
const sendToFlutter = (type: WebEventType, payload?: any) => { |
|
|
const sendToFlutter = (type: WebEventType, payload?: any) => { |
|
|
const event: WebEvent = { |
|
|
|
|
|
type, |
|
|
|
|
|
payload, |
|
|
|
|
|
timestamp: Date.now(), |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
if (typeof window === 'undefined') return; |
|
|
|
|
|
|
|
|
|
|
|
const action = WEB_EVENT_TO_ACTION[type]; |
|
|
|
|
|
let delivered = false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
// روش 1: postMessage (استاندارد)
|
|
|
|
|
|
|
|
|
// روش اصلی و واقعی: کانال 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) { |
|
|
if (window.parent && window.parent !== window) { |
|
|
window.parent.postMessage(event, '*'); |
|
|
window.parent.postMessage(event, '*'); |
|
|
addLog(`📤 ارسال به Flutter: ${type}`, 'success'); |
|
|
|
|
|
|
|
|
delivered = true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// روش 2: Flutter WebView interface (Android)
|
|
|
|
|
|
if ((window as any).FlutterChannel) { |
|
|
if ((window as any).FlutterChannel) { |
|
|
(window as any).FlutterChannel.postMessage(JSON.stringify(event)); |
|
|
(window as any).FlutterChannel.postMessage(JSON.stringify(event)); |
|
|
addLog(`📤 ارسال به Flutter (Android): ${type}`, 'success'); |
|
|
|
|
|
|
|
|
delivered = true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// روش 3: iOS WebKit message handler
|
|
|
|
|
|
if ((window as any).webkit?.messageHandlers?.FlutterChannel) { |
|
|
if ((window as any).webkit?.messageHandlers?.FlutterChannel) { |
|
|
(window as any).webkit.messageHandlers.FlutterChannel.postMessage(event); |
|
|
(window as any).webkit.messageHandlers.FlutterChannel.postMessage(event); |
|
|
addLog(`📤 ارسال به Flutter (iOS): ${type}`, 'success'); |
|
|
|
|
|
|
|
|
delivered = true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// اگر هیچ کدام موجود نبود
|
|
|
|
|
|
if ( |
|
|
|
|
|
(!window.parent || window.parent === window) && |
|
|
|
|
|
!(window as any).FlutterChannel && |
|
|
|
|
|
!(window as any).webkit?.messageHandlers?.FlutterChannel |
|
|
|
|
|
) { |
|
|
|
|
|
|
|
|
if (!delivered) { |
|
|
addLog(`⚠️ محیط Flutter یافت نشد - در حالت توسعه`, 'info'); |
|
|
addLog(`⚠️ محیط Flutter یافت نشد - در حالت توسعه`, 'info'); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
@ -185,6 +204,77 @@ export function useFlutterBridge( |
|
|
}; |
|
|
}; |
|
|
}, [onEvent, enableLogging]); |
|
|
}, [onEvent, enableLogging]); |
|
|
|
|
|
|
|
|
|
|
|
// تشخیص آمادگی واقعی + ارسال خودکار WEB_READY
|
|
|
|
|
|
// در WebView واقعی Flutter، شیء window.HabibApp توسط addJavaScriptChannel
|
|
|
|
|
|
// تزریق میشود؛ وجودش یعنی پل برقرار است. منتظر INITIAL_CONFIG نمیمانیم،
|
|
|
|
|
|
// چون Flutter چنین ایونتی نمیفرستد.
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (typeof window === 'undefined') return; |
|
|
|
|
|
|
|
|
|
|
|
let interval: ReturnType<typeof setInterval> | undefined; |
|
|
|
|
|
let timeout: ReturnType<typeof setTimeout> | 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 { |
|
|
return { |
|
|
events, |
|
|
events, |
|
|
lastEvent, |
|
|
lastEvent, |
|
|
|