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