diff --git a/desktop/public/formplayer-host-stub.js b/desktop/public/formplayer-host-stub.js index df3518a0a..c284f4060 100644 --- a/desktop/public/formplayer-host-stub.js +++ b/desktop/public/formplayer-host-stub.js @@ -34,4 +34,43 @@ } }, }; + + /** + * ODE Desktop: deliver bridge *_response to pending Formulus promises without + * relying on postMessage from the outer shell (unreliable for srcdoc iframes in WebView2). + */ + function deliverBridgeResponseBody(body) { + if (!body || !body.type || !body.messageId) { + return; + } + window.dispatchEvent( + new MessageEvent('message', { + data: JSON.stringify(body), + }), + ); + } + + window.__odeFormplayerDeliverBridgeResponse = function ( + requestType, + messageId, + payload, + ) { + var responseType = requestType + '_response'; + var body = { type: responseType, messageId: messageId }; + if (payload && typeof payload === 'object') { + if ('result' in payload) body.result = payload.result; + if ('error' in payload) body.error = payload.error; + } + deliverBridgeResponseBody(body); + }; + + /** Same-origin broadcast fallback when the outer shell cannot postMessage into srcdoc iframes (WebView2). */ + if (typeof BroadcastChannel !== 'undefined') { + var bridgeResponseChannel = new BroadcastChannel( + 'ode-formplayer-bridge-response', + ); + bridgeResponseChannel.onmessage = function (event) { + deliverBridgeResponseBody(event.data); + }; + } })(); diff --git a/desktop/public/formulus-injection.js b/desktop/public/formulus-injection.js index 4a93337c0..5e5f43381 100644 --- a/desktop/public/formulus-injection.js +++ b/desktop/public/formulus-injection.js @@ -235,8 +235,6 @@ const messageId = 'msg_' + Date.now() + '_' + Math.floor(Math.random() * 1000); - // Add response handler for methods that return values - const callback = event => { try { let data; diff --git a/desktop/src/components/CustomAppEmbed.tsx b/desktop/src/components/CustomAppEmbed.tsx index ac1e5b3b4..9542d418e 100644 --- a/desktop/src/components/CustomAppEmbed.tsx +++ b/desktop/src/components/CustomAppEmbed.tsx @@ -72,6 +72,8 @@ export type CustomAppEmbedProps = { /** Workspace-relative path to `index.html`; defaults from {@link mode}. */ indexRelativePath?: string; loadingLabel?: string; + /** Fired when the iframe document loads (bridge routing in WebView2). */ + onContentWindowReady?: (contentWindow: Window | null) => void; }; function defaultIndexRelativePath(mode: CustomAppEmbedMode): string { @@ -93,10 +95,12 @@ export const CustomAppEmbed = forwardRef< HTMLIFrameElement, CustomAppEmbedProps >(function CustomAppEmbed( - { mountKey, mode, indexRelativePath, loadingLabel }, + { mountKey, mode, indexRelativePath, loadingLabel, onContentWindowReady }, ref, ) { const innerRef = useRef(null); + const onContentWindowReadyRef = useRef(onContentWindowReady); + onContentWindowReadyRef.current = onContentWindowReady; const setRefs = useCallback( (el: HTMLIFrameElement | null) => { (innerRef as MutableRefObject).current = el; @@ -149,6 +153,7 @@ export const CustomAppEmbed = forwardRef< // hash for routing (HashRouter or path), so `#ode-…` would break the initial route. const url = `${indexAssetUrl}?ode=${Date.now()}`; el.onload = () => { + onContentWindowReadyRef.current?.(el.contentWindow); setLoading(false); }; el.src = url; diff --git a/desktop/src/components/FormplayerEmbed.tsx b/desktop/src/components/FormplayerEmbed.tsx index e655d2a4d..0b691fb4d 100644 --- a/desktop/src/components/FormplayerEmbed.tsx +++ b/desktop/src/components/FormplayerEmbed.tsx @@ -2,10 +2,11 @@ import { forwardRef, useCallback, useEffect, + useImperativeHandle, useRef, useState, - type MutableRefObject, } from 'react'; +import { postFormplayerBridgeReply } from '../lib/formPreviewBridge'; import type { FormInitData } from '../lib/formplayerHost'; const FORMSPLAYER_INDEX = `${import.meta.env.BASE_URL}formplayer_dist/index.html`; @@ -40,6 +41,18 @@ export type FormplayerEmbedProps = { /** Full `FormInitData` for the embedded formplayer; `null` shows `emptyMessage` only. */ formInitData: FormInitData | null; emptyMessage?: string; + /** Fired when the iframe document loads (used to register `contentWindow` for bridge routing). */ + onContentWindowReady?: (contentWindow: Window | null) => void; +}; + +/** Imperative handle for bridge delivery into the iframe document (WebView2-safe). */ +export type FormplayerEmbedHandle = { + getIframe: () => HTMLIFrameElement | null; + deliverBridgeResponse: ( + requestType: string, + messageId: string, + payload: { result?: unknown; error?: string }, + ) => void; }; /** @@ -48,27 +61,40 @@ export type FormplayerEmbedProps = { * base href) so Finalize / `submitObservation` and extension APIs work. */ export const FormplayerEmbed = forwardRef< - HTMLIFrameElement, + FormplayerEmbedHandle, FormplayerEmbedProps >(function FormplayerEmbed( { formInitData, emptyMessage = 'Select a form type and apply params/saved JSON to load the preview.', + onContentWindowReady, }, ref, ) { const innerRef = useRef(null); const timeoutRef = useRef(null); - const setRefs = useCallback( - (el: HTMLIFrameElement | null) => { - (innerRef as MutableRefObject).current = el; - if (typeof ref === 'function') { - ref(el); - } else if (ref) { - (ref as MutableRefObject).current = el; - } - }, - [ref], + const onContentWindowReadyRef = useRef(onContentWindowReady); + onContentWindowReadyRef.current = onContentWindowReady; + + useImperativeHandle( + ref, + () => ({ + getIframe: () => innerRef.current, + deliverBridgeResponse: (requestType, messageId, payload) => { + const win = innerRef.current?.contentWindow ?? null; + if (!win) { + return; + } + postFormplayerBridgeReply( + innerRef.current, + requestType, + messageId, + payload, + win, + ); + }, + }), + [], ); const [error, setError] = useState(null); @@ -122,6 +148,7 @@ export const FormplayerEmbed = forwardRef< window.clearTimeout(timeoutRef.current); timeoutRef.current = null; } + onContentWindowReadyRef.current?.(el.contentWindow); setLoading(false); }; // WebView2 can behave inconsistently with blob: + module scripts in packaged apps. @@ -160,7 +187,9 @@ export const FormplayerEmbed = forwardRef< {error ?

{error}

: null} {loading && !error ?

Loading formplayer…

: null}