diff --git a/formulus/App.tsx b/formulus/App.tsx index 259c3acee..fa4fe746e 100644 --- a/formulus/App.tsx +++ b/formulus/App.tsx @@ -19,6 +19,7 @@ import QRScannerModal from './src/components/QRScannerModal'; import SignatureCaptureModal from './src/components/SignatureCaptureModal'; import MainAppNavigator from './src/navigation/MainAppNavigator'; import { FormInitData } from './src/webview/FormulusInterfaceDefinition.ts'; +import { FormSpec } from './src/services'; /** * Inner component that consumes the AppTheme context to build a dynamic @@ -58,13 +59,58 @@ function AppInner(): React.JSX.Element { onResult: (result: unknown) => void; } | null>(null); - const [formplayerVisible, setFormplayerVisible] = useState(false); - const formplayerModalRef = React.useRef(null); - const formplayerVisibleRef = React.useRef(false); + type FormplayerStackEntry = { + id: string; + formSpec: FormSpec; + params: Record | null; + observationId: string | null; + savedData: Record | null; + operationId: string | null; + }; - useEffect(() => { - formplayerVisibleRef.current = formplayerVisible; - }, [formplayerVisible]); + const [formplayerStack, setFormplayerStack] = useState< + FormplayerStackEntry[] + >([]); + const formplayerModalRefs = React.useRef( + new Map(), + ); + + const initializeStackEntry = React.useCallback( + (entry: FormplayerStackEntry) => { + let attempt = 0; + + const tryInitialize = () => { + const modalHandle = formplayerModalRefs.current.get(entry.id); + if (!modalHandle) { + if (attempt < 20) { + attempt += 1; + setTimeout(tryInitialize, 100); + } + return; + } + + setTimeout(() => { + modalHandle.initializeForm( + entry.formSpec, + entry.params, + entry.observationId, + entry.savedData, + entry.operationId, + ); + }, 200); + }; + + tryInitialize(); + }, + [], + ); + + const closeFormplayerEntry = React.useCallback((entryId: string) => { + formplayerModalRefs.current.delete(entryId); + setFormplayerStack(current => + current.filter(entry => entry.id !== entryId), + ); + }, []); useEffect(() => { FormService.getInstance(); @@ -92,17 +138,6 @@ function AppInner(): React.JSX.Element { ); const handleOpenFormplayer = async (config: FormInitData) => { - // If formplayer is already visible, close it first to allow opening a new form - if (formplayerVisibleRef.current) { - console.log( - '[App] Formplayer already visible, closing first before opening new form', - ); - formplayerVisibleRef.current = false; - setFormplayerVisible(false); - // Wait for modal to close before proceeding - await new Promise(resolve => setTimeout(() => resolve(), 300)); - } - const { formType, observationId, params, savedData, operationId } = config; @@ -127,21 +162,20 @@ function AppInner(): React.JSX.Element { return; } - // Set visible state first to mount the modal - formplayerVisibleRef.current = true; - setFormplayerVisible(true); + const entryId = + operationId || + `${formType}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; + const entry: FormplayerStackEntry = { + id: entryId, + formSpec, + params: params || null, + observationId: observationId || null, + savedData: savedData || null, + operationId: operationId || null, + }; - // Wait for modal to mount and WebView to start loading before initializing form - // This ensures the WebView ref is available and the modal is visible - setTimeout(() => { - formplayerModalRef.current?.initializeForm( - formSpec, - params || null, - observationId || null, - savedData || null, - operationId || null, - ); - }, 200); + setFormplayerStack(current => [...current, entry]); + initializeStackEntry(entry); } catch (error) { console.error('[App] Error opening formplayer:', error); Alert.alert( @@ -150,15 +184,19 @@ function AppInner(): React.JSX.Element { error instanceof Error ? error.message : 'Unknown error' }`, ); - // Reset state on error - formplayerVisibleRef.current = false; - setFormplayerVisible(false); } }; const handleCloseFormplayer = () => { - formplayerVisibleRef.current = false; - setFormplayerVisible(false); + setFormplayerStack(current => { + if (current.length === 0) { + return current; + } + const next = current.slice(0, -1); + const removed = current[current.length - 1]; + formplayerModalRefs.current.delete(removed.id); + return next; + }); }; appEvents.addListener( @@ -182,7 +220,7 @@ function AppInner(): React.JSX.Element { ); appEvents.removeListener('closeFormplayer', handleCloseFormplayer); }; - }, []); + }, [closeFormplayerEntry, initializeStackEntry]); return ( <> @@ -192,14 +230,23 @@ function AppInner(): React.JSX.Element { /> - { - formplayerVisibleRef.current = false; - setFormplayerVisible(false); - }} - /> + {formplayerStack.map((entry, index) => ( + { + if (instance) { + formplayerModalRefs.current.set(entry.id, instance); + } else { + formplayerModalRefs.current.delete(entry.id); + } + }} + visible={true} + isActive={index === formplayerStack.length - 1} + onClose={() => { + closeFormplayerEntry(entry.id); + }} + /> + ))} void; sendFormInit: (formData: FormInitData) => Promise; sendAttachmentData: (attachmentData: File) => Promise; + notifyReceiveFocus: () => void; } interface CustomAppWebViewProps { @@ -340,6 +341,7 @@ const CustomAppWebView = forwardRef< messageManager.sendFormInit(formData), sendAttachmentData: (attachmentData: File) => messageManager.sendAttachmentData(attachmentData), + notifyReceiveFocus: () => messageManager.notifyReceiveFocus(), }), [messageManager], ); diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx index 11e349991..9813c5ac4 100644 --- a/formulus/src/components/FormplayerModal.tsx +++ b/formulus/src/components/FormplayerModal.tsx @@ -48,6 +48,7 @@ import { geolocationService } from '../services/GeolocationService'; interface FormplayerModalProps { visible: boolean; + isActive?: boolean; onClose: () => void; } @@ -67,7 +68,7 @@ export interface FormplayerModalHandle { } const FormplayerModal = forwardRef( - ({ visible, onClose }, ref) => { + ({ visible, isActive = true, onClose }, ref) => { const webViewRef = useRef(null); const [isSubmitting, setIsSubmitting] = useState(false); const { showConfirm } = useConfirmModal(); @@ -191,6 +192,7 @@ const FormplayerModal = forwardRef( // Track WebView ready state const [webViewReady, setWebViewReady] = useState(false); + const previousIsActiveRef = useRef(isActive); // Handle WebView load complete const handleWebViewLoad = () => { @@ -585,14 +587,16 @@ const FormplayerModal = forwardRef( // Register/unregister modal with message handlers and reset form state useEffect(() => { - if (visible) { + if (visible && isActive) { // Register this modal as the active one for handling submissions setActiveFormplayerModal({ handleSubmission }); } else { - // Unregister when modal is closed + // Inactive/hidden modals must not handle submissions. setActiveFormplayerModal(null); + } - // Reset form state when modal is closed + if (!visible) { + // Reset form state only when the modal actually closes. setTimeout(() => { setCurrentFormType(null); setCurrentFormDisplayName(null); @@ -603,7 +607,20 @@ const FormplayerModal = forwardRef( setWebViewReady(false); // Reset WebView ready state }, 300); // Small delay to ensure modal is fully closed } - }, [visible, handleSubmission]); + }, [visible, isActive, handleSubmission]); + + useEffect(() => { + if ( + visible && + isActive && + webViewReady && + currentFormType && + previousIsActiveRef.current === false + ) { + webViewRef.current?.notifyReceiveFocus(); + } + previousIsActiveRef.current = isActive; + }, [visible, isActive, webViewReady, currentFormType]); useImperativeHandle(ref, () => ({ initializeForm, handleSubmission })); diff --git a/formulus/src/webview/FormulusWebViewHandler.ts b/formulus/src/webview/FormulusWebViewHandler.ts index b04f511c6..e1bfc8a1f 100644 --- a/formulus/src/webview/FormulusWebViewHandler.ts +++ b/formulus/src/webview/FormulusWebViewHandler.ts @@ -264,7 +264,30 @@ export class FormulusWebViewMessageManager { } } + public notifyReceiveFocus(): void { + if (!this.webViewRef.current || !this.isWebViewReady) { + return; + } + + this.webViewRef.current.injectJavaScript(` + (function() { + try { + if (typeof window.onReceiveFocus === 'function') { + Promise.resolve(window.onReceiveFocus()).catch(function(error) { + console.error('Error in window.onReceiveFocus:', error); + }); + } + } catch (error) { + console.error('Error invoking window.onReceiveFocus:', error); + } + })(); + true; + `); + } + public handleReceiveFocus(): void { + this.notifyReceiveFocus(); + // Optionally call native-side handler if it exists for onReceiveFocus if (this.nativeSideHandlers.onReceiveFocus) { try {