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
8.5 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 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<typeof setInterval> | undefined;
let timeout: ReturnType<typeof setTimeout> | 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,
};
}