You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
9.2 KiB
285 lines
9.2 KiB
/**
|
|
* 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<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(
|
|
options: UseFlutterBridgeOptions = {}
|
|
): UseFlutterBridgeReturn {
|
|
const { onEvent, enableLogging = true } = options;
|
|
|
|
const [events, setEvents] = useState<FlutterEvent[]>([]);
|
|
const [lastEvent, setLastEvent] = useState<FlutterEvent | null>(null);
|
|
const [isReady, setIsReady] = useState(false);
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
|
|
const logsRef = useRef<string[]>([]);
|
|
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<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 {
|
|
events,
|
|
lastEvent,
|
|
sendToFlutter,
|
|
isReady,
|
|
logs,
|
|
};
|
|
}
|