Browse Source

bridge test webview

master
mortezaei 2 weeks ago
parent
commit
7bda79977c
  1. 308
      src/app/[lang]/test-webview/page.tsx
  2. 133
      src/components/ui/report-actions-sheet.tsx
  3. 195
      src/hooks/useFlutterBridge.ts
  4. 3
      src/types/window.d.ts

308
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<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: "habib@gmail.com",
})
}
>
👨 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>
);
}

133
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) {
>
<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>

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

3
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<string, unknown>) => void;
}
}

Loading…
Cancel
Save