4 changed files with 633 additions and 6 deletions
-
308src/app/[lang]/test-webview/page.tsx
-
133src/components/ui/report-actions-sheet.tsx
-
195src/hooks/useFlutterBridge.ts
-
3src/types/window.d.ts
@ -0,0 +1,308 @@ |
|||
"use client"; |
|||
|
|||
import { useState, useEffect } from "react"; |
|||
import { useFlutterBridge } from "@/hooks/useFlutterBridge"; |
|||
import Button from "@/components/ui/button"; |
|||
|
|||
export default function TestWebViewPage() { |
|||
const { sendToFlutter, logs, lastEvent, events, isReady } = useFlutterBridge({ |
|||
enableLogging: true, |
|||
}); |
|||
|
|||
const [config, setConfig] = useState<any>(null); |
|||
|
|||
useEffect(() => { |
|||
// شناسایی INITIAL_CONFIG
|
|||
const initialConfig = events.find((e) => e.type === "INITIAL_CONFIG"); |
|||
if (initialConfig) { |
|||
setConfig(initialConfig.payload); |
|||
} |
|||
}, [events]); |
|||
|
|||
// تابع تست برای شبیهسازی ایونتهای Flutter
|
|||
const simulateFlutterEvent = (type: string, payload: any) => { |
|||
window.dispatchEvent( |
|||
new CustomEvent("flutterEvent", { |
|||
detail: { type, payload, timestamp: Date.now() }, |
|||
}) |
|||
); |
|||
}; |
|||
|
|||
return ( |
|||
<div className="min-h-screen bg-gray-50 p-4"> |
|||
<div className="max-w-4xl mx-auto"> |
|||
{/* Header */} |
|||
<div className="bg-white rounded-lg shadow-md p-6 mb-4"> |
|||
<h1 className="text-2xl font-bold text-gray-800 mb-2"> |
|||
🧪 Flutter WebView Bridge Test |
|||
</h1> |
|||
<div className="flex items-center gap-3"> |
|||
<div |
|||
className={`w-3 h-3 rounded-full ${isReady ? "bg-green-500 animate-pulse" : "bg-red-500"}`} |
|||
/> |
|||
<span className="text-sm text-gray-600"> |
|||
{isReady ? "✅ Bridge Ready" : "⏳ Waiting for Flutter..."} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|||
{/* کارت 1: ارسال ایونتها */} |
|||
<div className="bg-white rounded-lg shadow-md p-6"> |
|||
<h2 className="text-lg font-bold text-gray-800 mb-4"> |
|||
📤 ارسال به Flutter |
|||
</h2> |
|||
<div className="space-y-2"> |
|||
<Button |
|||
onClick={() => |
|||
sendToFlutter("WEB_READY", { |
|||
timestamp: Date.now(), |
|||
url: window.location.href, |
|||
}) |
|||
} |
|||
> |
|||
🚀 WEB_READY |
|||
</Button> |
|||
<Button onClick={() => sendToFlutter("REQUEST_LOCATION")}> |
|||
📍 REQUEST_LOCATION |
|||
</Button> |
|||
<Button |
|||
onClick={() => |
|||
sendToFlutter("REQUEST_CONSULTANT", { |
|||
consultant: "[email protected]", |
|||
}) |
|||
} |
|||
> |
|||
👨⚕️ REQUEST_CONSULTANT |
|||
</Button> |
|||
<Button |
|||
onClick={() => |
|||
sendToFlutter("PAGE_LOADED", { |
|||
page: window.location.pathname, |
|||
}) |
|||
} |
|||
> |
|||
📄 PAGE_LOADED |
|||
</Button> |
|||
<Button |
|||
onClick={() => |
|||
sendToFlutter("ERROR_OCCURRED", { |
|||
message: "Test error", |
|||
code: "TEST_ERROR", |
|||
}) |
|||
} |
|||
> |
|||
❌ ERROR_OCCURRED |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* کارت 2: شبیهسازی ایونتهای Flutter */} |
|||
<div className="bg-white rounded-lg shadow-md p-6"> |
|||
<h2 className="text-lg font-bold text-gray-800 mb-4"> |
|||
📥 شبیهسازی Flutter |
|||
</h2> |
|||
<p className="text-xs text-gray-600 mb-3"> |
|||
برای تست بدون Flutter از این دکمهها استفاده کنید |
|||
</p> |
|||
<div className="space-y-2"> |
|||
<button |
|||
onClick={() => |
|||
simulateFlutterEvent("INITIAL_CONFIG", { |
|||
version: "1.0.0", |
|||
layout: { |
|||
breakpoint: "mobile", |
|||
screenWidth: 375, |
|||
screenHeight: 812, |
|||
isMobile: true, |
|||
}, |
|||
safeArea: { top: 44, bottom: 34, left: 0, right: 0 }, |
|||
platform: { os: "ios", version: "17.2" }, |
|||
locale: { languageCode: "fa", isRtl: true }, |
|||
}) |
|||
} |
|||
className="w-full bg-blue-500 hover:bg-blue-600 text-white text-sm py-2 px-4 rounded-lg transition" |
|||
> |
|||
🔧 INITIAL_CONFIG |
|||
</button> |
|||
<button |
|||
onClick={() => |
|||
simulateFlutterEvent("KEYBOARD_CHANGED", { |
|||
visible: true, |
|||
height: 336, |
|||
animationDuration: 250, |
|||
}) |
|||
} |
|||
className="w-full bg-blue-500 hover:bg-blue-600 text-white text-sm py-2 px-4 rounded-lg transition" |
|||
> |
|||
⌨️ KEYBOARD_CHANGED |
|||
</button> |
|||
<button |
|||
onClick={() => |
|||
simulateFlutterEvent("ORIENTATION_CHANGED", { |
|||
orientation: "landscape", |
|||
screenWidth: 812, |
|||
screenHeight: 375, |
|||
}) |
|||
} |
|||
className="w-full bg-blue-500 hover:bg-blue-600 text-white text-sm py-2 px-4 rounded-lg transition" |
|||
> |
|||
📱 ORIENTATION_CHANGED |
|||
</button> |
|||
<button |
|||
onClick={() => |
|||
simulateFlutterEvent("SAFE_AREA_CHANGED", { |
|||
top: 0, |
|||
bottom: 0, |
|||
left: 0, |
|||
right: 0, |
|||
}) |
|||
} |
|||
className="w-full bg-blue-500 hover:bg-blue-600 text-white text-sm py-2 px-4 rounded-lg transition" |
|||
> |
|||
🔒 SAFE_AREA_CHANGED |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* کارت 3: آخرین ایونت */} |
|||
{lastEvent && ( |
|||
<div className="bg-green-50 rounded-lg shadow-md p-6 border-2 border-green-200"> |
|||
<h2 className="text-lg font-bold text-green-800 mb-4"> |
|||
📥 آخرین ایونت دریافتی |
|||
</h2> |
|||
<div className="bg-white p-3 rounded border border-green-300"> |
|||
<div className="font-mono text-xs"> |
|||
<div className="text-green-600 font-bold mb-2"> |
|||
Type: {lastEvent.type} |
|||
</div> |
|||
<div className="text-gray-600"> |
|||
<pre className="whitespace-pre-wrap break-all"> |
|||
{JSON.stringify(lastEvent.payload, null, 2)} |
|||
</pre> |
|||
</div> |
|||
<div className="text-gray-400 mt-2 text-[10px]"> |
|||
{new Date(lastEvent.timestamp || Date.now()).toLocaleString( |
|||
"fa-IR" |
|||
)} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
|
|||
{/* کارت 4: Config دریافتی */} |
|||
{config && ( |
|||
<div className="bg-purple-50 rounded-lg shadow-md p-6 border-2 border-purple-200"> |
|||
<h2 className="text-lg font-bold text-purple-800 mb-4"> |
|||
⚙️ Initial Config |
|||
</h2> |
|||
<div className="bg-white p-3 rounded border border-purple-300 max-h-[400px] overflow-auto"> |
|||
<pre className="font-mono text-xs text-gray-600 whitespace-pre-wrap break-all"> |
|||
{JSON.stringify(config, null, 2)} |
|||
</pre> |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
|
|||
{/* لاگها */} |
|||
<div className="bg-white rounded-lg shadow-md p-6 mt-4"> |
|||
<div className="flex items-center justify-between mb-4"> |
|||
<h2 className="text-lg font-bold text-gray-800"> |
|||
📋 لاگها ({logs.length}) |
|||
</h2> |
|||
<button |
|||
onClick={() => { |
|||
const logsText = logs.join("\n"); |
|||
navigator.clipboard.writeText(logsText); |
|||
alert("لاگها کپی شدند!"); |
|||
}} |
|||
className="text-sm text-blue-600 hover:text-blue-800" |
|||
> |
|||
📋 کپی |
|||
</button> |
|||
</div> |
|||
<div className="bg-gray-50 rounded border border-gray-200 p-3 max-h-[400px] overflow-auto"> |
|||
{logs.length === 0 ? ( |
|||
<p className="text-sm text-gray-500 text-center py-4"> |
|||
هنوز لاگی ثبت نشده است |
|||
</p> |
|||
) : ( |
|||
<div className="space-y-1"> |
|||
{logs.map((log, index) => ( |
|||
<div |
|||
key={index} |
|||
className={`font-mono text-xs p-2 rounded ${ |
|||
log.includes("[ERROR]") |
|||
? "bg-red-100 text-red-800" |
|||
: log.includes("[SUCCESS]") |
|||
? "bg-green-100 text-green-800" |
|||
: "bg-white text-gray-700" |
|||
}`}
|
|||
> |
|||
{log} |
|||
</div> |
|||
))} |
|||
</div> |
|||
)} |
|||
</div> |
|||
</div> |
|||
|
|||
{/* راهنما */} |
|||
<div className="bg-blue-50 rounded-lg shadow-md p-6 mt-4 border-2 border-blue-200"> |
|||
<h2 className="text-lg font-bold text-blue-800 mb-4">💡 راهنما</h2> |
|||
<div className="space-y-2 text-sm text-blue-900"> |
|||
<p className="font-semibold">🎯 نحوه تست:</p> |
|||
<ol className="list-decimal list-inside space-y-1 mr-2"> |
|||
<li>اگر در Flutter هستید، صفحه را load کنید</li> |
|||
<li> |
|||
Flutter باید INITIAL_CONFIG بفرستد (اگر نفرستاد، از دکمه |
|||
شبیهسازی استفاده کنید) |
|||
</li> |
|||
<li>دکمه WEB_READY را بزنید تا به Flutter اطلاع دهید</li> |
|||
<li>سایر دکمهها را تست کنید</li> |
|||
<li>لاگها را در Console و در این صفحه مشاهده کنید</li> |
|||
</ol> |
|||
<p className="font-semibold mt-4">🔧 بدون Flutter:</p> |
|||
<p> |
|||
از دکمههای "شبیهسازی Flutter" استفاده کنید تا ببینید WebView چگونه |
|||
react میکند |
|||
</p> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* تمام ایونتها */} |
|||
{events.length > 0 && ( |
|||
<div className="bg-white rounded-lg shadow-md p-6 mt-4"> |
|||
<h2 className="text-lg font-bold text-gray-800 mb-4"> |
|||
📜 تاریخچه ایونتها ({events.length}) |
|||
</h2> |
|||
<div className="space-y-2 max-h-[300px] overflow-auto"> |
|||
{events.map((event, index) => ( |
|||
<div |
|||
key={index} |
|||
className="bg-gray-50 rounded border border-gray-200 p-3" |
|||
> |
|||
<div className="flex items-center justify-between mb-1"> |
|||
<span className="font-bold text-sm text-gray-800"> |
|||
{event.type} |
|||
</span> |
|||
<span className="text-xs text-gray-500"> |
|||
{new Date(event.timestamp || Date.now()).toLocaleTimeString( |
|||
"fa-IR" |
|||
)} |
|||
</span> |
|||
</div> |
|||
<pre className="font-mono text-xs text-gray-600 whitespace-pre-wrap break-all"> |
|||
{JSON.stringify(event.payload, null, 2)} |
|||
</pre> |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
@ -2,6 +2,7 @@ |
|||
|
|||
import { useEffect, useState } from "react"; |
|||
import Button from "@/components/ui/button"; |
|||
import { useFlutterBridge } from "@/hooks/useFlutterBridge"; |
|||
|
|||
const EXIT_ANIMATION_MS = 220; |
|||
|
|||
@ -13,20 +14,41 @@ export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) { |
|||
const [isVisible, setIsVisible] = useState(true); |
|||
const [isEntering, setIsEntering] = useState(true); |
|||
const [isClosing, setIsClosing] = useState(false); |
|||
const [showLogs, setShowLogs] = useState(false); |
|||
|
|||
const { sendToFlutter, logs, lastEvent, isReady } = useFlutterBridge({ |
|||
enableLogging: true, |
|||
}); |
|||
|
|||
const closeSheet = () => { |
|||
if (isClosing) return; |
|||
setIsClosing(true); |
|||
}; |
|||
|
|||
// ✅ دکمه WEB_READY
|
|||
const handleSendWebReady = () => { |
|||
sendToFlutter("WEB_READY", { |
|||
timestamp: Date.now(), |
|||
url: window.location.href, |
|||
userAgent: navigator.userAgent, |
|||
}); |
|||
console.log("✅ WEB_READY ارسال شد"); |
|||
}; |
|||
|
|||
// ✅ دکمه دریافت موقعیت مکانی
|
|||
const handleGetLocation = () => { |
|||
// روش قدیمی
|
|||
if (typeof window !== "undefined" && window.HabibApp) { |
|||
window.HabibApp.postMessage(JSON.stringify({ action: "get_location" })); |
|||
closeSheet(); |
|||
} |
|||
// روش جدید با bridge
|
|||
sendToFlutter("REQUEST_LOCATION"); |
|||
console.log("📍 REQUEST_LOCATION ارسال شد"); |
|||
}; |
|||
|
|||
// ✅ دکمه مشاور
|
|||
const handleOpenConsultant = () => { |
|||
// روش قدیمی
|
|||
if (typeof window !== "undefined" && window.HabibApp) { |
|||
window.HabibApp.postMessage( |
|||
JSON.stringify({ |
|||
@ -34,8 +56,12 @@ export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) { |
|||
data: { consultant: "[email protected]" }, |
|||
}) |
|||
); |
|||
closeSheet(); |
|||
} |
|||
// روش جدید با bridge
|
|||
sendToFlutter("REQUEST_CONSULTANT", { |
|||
consultant: "[email protected]", |
|||
}); |
|||
console.log("👨⚕️ REQUEST_CONSULTANT ارسال شد"); |
|||
}; |
|||
|
|||
useEffect(() => { |
|||
@ -101,15 +127,110 @@ export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) { |
|||
> |
|||
<section |
|||
className={[ |
|||
"w-full sm:max-w-[375px] rounded-t-[15px] bg-[#F9F8F8] p-3.5 shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out", |
|||
"w-full sm:max-w-[375px] rounded-t-[15px] bg-[#F9F8F8] shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out max-h-[85vh] overflow-y-auto", |
|||
isClosing || isEntering ? "translate-y-full" : "translate-y-0", |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
> |
|||
<div className="flex flex-col gap-3"> |
|||
<Button onClick={handleGetLocation}>دریافت موقعیت مکانی</Button> |
|||
<Button onClick={handleOpenConsultant}>مشاوره با حبیب</Button> |
|||
{/* Header */} |
|||
<div className="sticky top-0 bg-[#F9F8F8] p-3.5 border-b border-gray-200/50"> |
|||
<div className="flex items-center justify-between"> |
|||
<div className="flex items-center gap-2"> |
|||
<h3 className="font-semibold text-gray-800">تنظیمات و پشتیبانی</h3> |
|||
<div |
|||
className={`w-2 h-2 rounded-full ${isReady ? "bg-green-500" : "bg-gray-400"}`} |
|||
title={isReady ? "متصل به Flutter" : "غیرفعال"} |
|||
/> |
|||
</div> |
|||
<button |
|||
onClick={closeSheet} |
|||
className="text-2xl text-gray-500 hover:text-gray-700 leading-none" |
|||
aria-label="بستن" |
|||
> |
|||
× |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="p-3.5 flex flex-col gap-3"> |
|||
{/* دکمههای اصلی */} |
|||
<div className="flex flex-col gap-2"> |
|||
<Button onClick={handleGetLocation}>📍 دریافت موقعیت مکانی</Button> |
|||
<Button onClick={handleOpenConsultant}>👨⚕️ مشاوره با حبیب</Button> |
|||
</div> |
|||
|
|||
{/* دکمه تست WEB_READY */} |
|||
<div className="bg-blue-50 rounded-lg p-3 border border-blue-200"> |
|||
<p className="text-xs text-blue-800 mb-2 font-semibold"> |
|||
🧪 تست ارتباط با Flutter: |
|||
</p> |
|||
<Button onClick={handleSendWebReady}> |
|||
🚀 ارسال WEB_READY |
|||
</Button> |
|||
</div> |
|||
|
|||
{/* نمایش آخرین ایونت دریافتی */} |
|||
{lastEvent && ( |
|||
<div className="bg-green-50 rounded-lg p-3 border border-green-200"> |
|||
<p className="text-xs text-green-800 font-semibold mb-2"> |
|||
📥 آخرین ایونت از Flutter: |
|||
</p> |
|||
<div className="bg-white rounded p-2 text-xs font-mono break-all"> |
|||
<div className="text-green-600 font-bold"> |
|||
{lastEvent.type} |
|||
</div> |
|||
<div className="text-gray-600 mt-1"> |
|||
{JSON.stringify(lastEvent.payload, null, 2)} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
|
|||
{/* لاگها */} |
|||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200"> |
|||
<div className="flex items-center justify-between mb-2"> |
|||
<p className="text-xs text-gray-700 font-semibold"> |
|||
📋 لاگها ({logs.length}) |
|||
</p> |
|||
<button |
|||
onClick={() => setShowLogs(!showLogs)} |
|||
className="text-xs text-blue-600 hover:text-blue-800" |
|||
> |
|||
{showLogs ? "مخفی کردن" : "نمایش"} |
|||
</button> |
|||
</div> |
|||
{showLogs && ( |
|||
<div className="max-h-[150px] overflow-y-auto space-y-1"> |
|||
{logs.length === 0 ? ( |
|||
<p className="text-xs text-gray-500 text-center py-2"> |
|||
هنوز لاگی ثبت نشده |
|||
</p> |
|||
) : ( |
|||
logs.slice(-10).map((log, index) => ( |
|||
<div |
|||
key={index} |
|||
className="text-[10px] font-mono bg-white p-1.5 rounded border border-gray-300 break-all" |
|||
> |
|||
{log} |
|||
</div> |
|||
)) |
|||
)} |
|||
</div> |
|||
)} |
|||
</div> |
|||
|
|||
{/* راهنما */} |
|||
<div className="bg-yellow-50 rounded-lg p-3 border border-yellow-200"> |
|||
<p className="text-xs text-yellow-900"> |
|||
<strong>💡 راهنما:</strong> |
|||
<br /> |
|||
• دکمه "WEB_READY" را بزنید |
|||
<br /> |
|||
• در Flutter console لاگ را ببینید |
|||
<br />• Flutter باید ایونت "INITIAL_CONFIG" بفرستد |
|||
</p> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
|
|||
@ -0,0 +1,195 @@ |
|||
/** |
|||
* 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[]; |
|||
} |
|||
|
|||
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) => { |
|||
const event: WebEvent = { |
|||
type, |
|||
payload, |
|||
timestamp: Date.now(), |
|||
}; |
|||
|
|||
try { |
|||
// روش 1: postMessage (استاندارد)
|
|||
if (window.parent && window.parent !== window) { |
|||
window.parent.postMessage(event, '*'); |
|||
addLog(`📤 ارسال به Flutter: ${type}`, 'success'); |
|||
} |
|||
|
|||
// روش 2: Flutter WebView interface (Android)
|
|||
if ((window as any).FlutterChannel) { |
|||
(window as any).FlutterChannel.postMessage(JSON.stringify(event)); |
|||
addLog(`📤 ارسال به Flutter (Android): ${type}`, 'success'); |
|||
} |
|||
|
|||
// روش 3: iOS WebKit message handler
|
|||
if ((window as any).webkit?.messageHandlers?.FlutterChannel) { |
|||
(window as any).webkit.messageHandlers.FlutterChannel.postMessage(event); |
|||
addLog(`📤 ارسال به Flutter (iOS): ${type}`, 'success'); |
|||
} |
|||
|
|||
// اگر هیچ کدام موجود نبود
|
|||
if ( |
|||
(!window.parent || window.parent === window) && |
|||
!(window as any).FlutterChannel && |
|||
!(window as any).webkit?.messageHandlers?.FlutterChannel |
|||
) { |
|||
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]); |
|||
|
|||
return { |
|||
events, |
|||
lastEvent, |
|||
sendToFlutter, |
|||
isReady, |
|||
logs, |
|||
}; |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue