"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTheme } from "@/components/theme/app-theme"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; import { resolveWebAppAppearance } from "./web-app-appearance"; type WebAppFrameProps = { /** A short-lived capability URL returned by the backend. */ runtimeUrl?: string | null; /** The host-owned accessible name. Runtime content is untrusted. */ title: string; className?: string; onLoad?: () => void; onError?: () => void; }; type FrameState = "loading" | "ready" | "unavailable"; /** * Hosts a static plugin web application without bringing it into the SPA * process. The sandbox deliberately omits allow-same-origin, top navigation, * popups, downloads, and a privileged host bridge. */ export function WebAppFrame({ runtimeUrl, title, className, onLoad, onError }: WebAppFrameProps) { const { isMobile } = useResponsiveBreakpoint(); const { t } = useTranslation(); const { resolvedTheme } = useTheme(); const [frameState, setFrameState] = useState(runtimeUrl ? "loading" : "unavailable"); const iframeRef = useRef(null); const frameReadyRef = useRef(false); useEffect(() => { frameReadyRef.current = false; setFrameState(runtimeUrl ? "loading" : "unavailable"); }, [runtimeUrl]); const sendAppearance = useCallback(() => { const target = iframeRef.current?.contentWindow; if (!target) return; target.postMessage(resolveWebAppAppearance(document, resolvedTheme), "*"); }, [resolvedTheme]); useEffect(() => { if (frameReadyRef.current) sendAppearance(); }, [sendAppearance]); const handleLoad = () => { sendAppearance(); frameReadyRef.current = true; onLoad?.(); window.requestAnimationFrame(() => setFrameState("ready")); }; const handleError = () => { frameReadyRef.current = false; setFrameState("unavailable"); onError?.(); }; return (
{runtimeUrl ? (