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

/**
* 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,
};
}