import { useEffect, useRef, useState } from "react"; declare global { interface Window { __zendegi__?: { getState: () => string; onEvent: (json: string) => void; updateState?: (json: string) => void; }; _flutter?: { buildConfig?: unknown; loader: { load: (config?: { config?: { entrypointBaseUrl?: string; assetBase?: string; }; onEntrypointLoaded?: (engineInitializer: { initializeEngine: (config: { hostElement: HTMLElement; assetBase?: string; }) => Promise<{ runApp: () => void }>; }) => Promise; }) => Promise; }; }; } } type FlutterViewProps = { state: Record; onEvent: (event: { type: string; payload?: Record }) => void; className?: string; height?: number; }; export function FlutterView({ state, onEvent, className, height, }: FlutterViewProps) { const containerRef = useRef(null); const [status, setStatus] = useState<"loading" | "ready" | "error">( "loading" ); const stateRef = useRef(state); stateRef.current = state; const onEventRef = useRef(onEvent); onEventRef.current = onEvent; useEffect(() => { const container = containerRef.current; if (!container) return; let pollingInterval: ReturnType | undefined; let pollingTimeout: ReturnType | undefined; window.__zendegi__ = { getState: () => JSON.stringify(stateRef.current), onEvent: (json: string) => { const event = JSON.parse(json) as { type: string; payload?: Record; }; onEventRef.current(event); }, }; const init = async () => { // Load flutter.js if not already loaded if (!window._flutter) { await new Promise((resolve, reject) => { if (document.querySelector('script[src="/flutter/flutter.js"]')) { // Script tag exists but hasn't finished — wait for _flutter to appear pollingTimeout = setTimeout(() => { clearInterval(pollingInterval); reject( new Error("Timed out waiting for flutter.js to initialize") ); }, 10_000); pollingInterval = setInterval(() => { if (window._flutter) { clearInterval(pollingInterval); clearTimeout(pollingTimeout); resolve(); } }, 50); return; } const script = document.createElement("script"); script.src = "/flutter/flutter.js"; script.defer = true; script.onload = () => resolve(); script.onerror = () => reject(new Error("Failed to load flutter.js")); document.head.appendChild(script); }); } // Fetch and set buildConfig so load() works (supports wasm) if (!window._flutter!.buildConfig) { const res = await fetch("/flutter/build_config.json"); if (!res.ok) { throw new Error(`Failed to fetch build config: ${res.status}`); } window._flutter!.buildConfig = await res.json(); } try { await window._flutter!.loader.load({ config: { entrypointBaseUrl: "/flutter/", assetBase: "/flutter/", }, onEntrypointLoaded: async (engineInitializer) => { const appRunner = await engineInitializer.initializeEngine({ hostElement: container, assetBase: "/flutter/", }); appRunner.runApp(); setStatus("ready"); }, }); } catch (error) { throw new Error( `Flutter engine initialization failed: ${error instanceof Error ? error.message : error}` ); } }; init().catch(() => setStatus("error")); return () => { clearInterval(pollingInterval); clearTimeout(pollingTimeout); container.replaceChildren(); delete (window as Partial).__zendegi__; }; }, []); useEffect(() => { window.__zendegi__?.updateState?.(JSON.stringify(state)); }, [state]); return (
{status === "loading" && (
Loading Flutter...
)} {status === "error" && (
Failed to load Flutter view
)}
); }