diff --git a/src/app/[lang]/test-webview/page.tsx b/src/app/[lang]/test-webview/page.tsx new file mode 100644 index 0000000..03538ce --- /dev/null +++ b/src/app/[lang]/test-webview/page.tsx @@ -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(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 ( +
+
+ {/* Header */} +
+

+ 🧪 Flutter WebView Bridge Test +

+
+
+ + {isReady ? "✅ Bridge Ready" : "⏳ Waiting for Flutter..."} + +
+
+ +
+ {/* کارت 1: ارسال ایونت‌ها */} +
+

+ 📤 ارسال به Flutter +

+
+ + + + + +
+
+ + {/* کارت 2: شبیه‌سازی ایونت‌های Flutter */} +
+

+ 📥 شبیه‌سازی Flutter +

+

+ برای تست بدون Flutter از این دکمه‌ها استفاده کنید +

+
+ + + + +
+
+ + {/* کارت 3: آخرین ایونت */} + {lastEvent && ( +
+

+ 📥 آخرین ایونت دریافتی +

+
+
+
+ Type: {lastEvent.type} +
+
+
+                      {JSON.stringify(lastEvent.payload, null, 2)}
+                    
+
+
+ {new Date(lastEvent.timestamp || Date.now()).toLocaleString( + "fa-IR" + )} +
+
+
+
+ )} + + {/* کارت 4: Config دریافتی */} + {config && ( +
+

+ ⚙️ Initial Config +

+
+
+                  {JSON.stringify(config, null, 2)}
+                
+
+
+ )} +
+ + {/* لاگ‌ها */} +
+
+

+ 📋 لاگ‌ها ({logs.length}) +

+ +
+
+ {logs.length === 0 ? ( +

+ هنوز لاگی ثبت نشده است +

+ ) : ( +
+ {logs.map((log, index) => ( +
+ {log} +
+ ))} +
+ )} +
+
+ + {/* راهنما */} +
+

💡 راهنما

+
+

🎯 نحوه تست:

+
    +
  1. اگر در Flutter هستید، صفحه را load کنید
  2. +
  3. + Flutter باید INITIAL_CONFIG بفرستد (اگر نفرستاد، از دکمه + شبیه‌سازی استفاده کنید) +
  4. +
  5. دکمه WEB_READY را بزنید تا به Flutter اطلاع دهید
  6. +
  7. سایر دکمه‌ها را تست کنید
  8. +
  9. لاگ‌ها را در Console و در این صفحه مشاهده کنید
  10. +
+

🔧 بدون Flutter:

+

+ از دکمه‌های "شبیه‌سازی Flutter" استفاده کنید تا ببینید WebView چگونه + react می‌کند +

+
+
+ + {/* تمام ایونت‌ها */} + {events.length > 0 && ( +
+

+ 📜 تاریخچه ایونت‌ها ({events.length}) +

+
+ {events.map((event, index) => ( +
+
+ + {event.type} + + + {new Date(event.timestamp || Date.now()).toLocaleTimeString( + "fa-IR" + )} + +
+
+                    {JSON.stringify(event.payload, null, 2)}
+                  
+
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/src/components/ui/report-actions-sheet.tsx b/src/components/ui/report-actions-sheet.tsx index 01d5057..0a68792 100644 --- a/src/components/ui/report-actions-sheet.tsx +++ b/src/components/ui/report-actions-sheet.tsx @@ -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: "habib@gmail.com" }, }) ); - closeSheet(); } + // روش جدید با bridge + sendToFlutter("REQUEST_CONSULTANT", { + consultant: "habib@gmail.com", + }); + console.log("👨‍⚕️ REQUEST_CONSULTANT ارسال شد"); }; useEffect(() => { @@ -101,15 +127,110 @@ export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) { >
-
- - + {/* Header */} +
+
+
+

تنظیمات و پشتیبانی

+
+
+ +
+
+ +
+ {/* دکمه‌های اصلی */} +
+ + +
+ + {/* دکمه تست WEB_READY */} +
+

+ 🧪 تست ارتباط با Flutter: +

+ +
+ + {/* نمایش آخرین ایونت دریافتی */} + {lastEvent && ( +
+

+ 📥 آخرین ایونت از Flutter: +

+
+
+ {lastEvent.type} +
+
+ {JSON.stringify(lastEvent.payload, null, 2)} +
+
+
+ )} + + {/* لاگ‌ها */} +
+
+

+ 📋 لاگ‌ها ({logs.length}) +

+ +
+ {showLogs && ( +
+ {logs.length === 0 ? ( +

+ هنوز لاگی ثبت نشده +

+ ) : ( + logs.slice(-10).map((log, index) => ( +
+ {log} +
+ )) + )} +
+ )} +
+ + {/* راهنما */} +
+

+ 💡 راهنما: +
+ • دکمه "WEB_READY" را بزنید +
+ • در Flutter console لاگ را ببینید +
• Flutter باید ایونت "INITIAL_CONFIG" بفرستد +

+
diff --git a/src/hooks/useFlutterBridge.ts b/src/hooks/useFlutterBridge.ts new file mode 100644 index 0000000..42f7b4a --- /dev/null +++ b/src/hooks/useFlutterBridge.ts @@ -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([]); + const [lastEvent, setLastEvent] = useState(null); + const [isReady, setIsReady] = useState(false); + const [logs, setLogs] = useState([]); + + const logsRef = useRef([]); + + // تابع کمکی برای اضافه کردن لاگ + 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, + }; +} diff --git a/src/types/window.d.ts b/src/types/window.d.ts index 355ab32..b3f4630 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -9,6 +9,8 @@ declare global { bottom?: number; left?: number; right?: number; + message?: string; + timestamp?: number; }; } @@ -23,6 +25,7 @@ declare global { HabibApp?: { postMessage: (message: string) => void; }; + sendToFlutter?: (action: string, data?: Record) => void; } }