UNPKG

@awell-health/navi-js-react

Version:

React components and hooks for integrating Navi care flows

202 lines (198 loc) โ€ข 8.71 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { createContext, useState, useEffect, useContext, useRef } from 'react'; import { loadNavi } from '@awell-health/navi-js'; const NaviContext = createContext(null); function NaviProvider({ publishableKey, branding = {}, children, config = {}, }) { const [loadError, setLoadError] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [navi, setNavi] = useState(null); useEffect(() => { setIsLoading(true); loadNavi(publishableKey, config) .then((navi) => { setNavi(navi); console.log("โœ… Navi SDK loaded successfully"); setIsLoading(false); setIsInitialized(true); }) .catch((error) => { console.error("โŒ Failed to load Navi SDK:", error); setLoadError(error.message); setIsInitialized(false); }); }, []); const value = { branding, initialized: isInitialized, loading: isLoading, error: loadError, publishableKey, navi, }; return jsx(NaviContext.Provider, { value: value, children: children }); } function useNavi() { const context = useContext(NaviContext); if (!context) { throw new Error("useNavi must be used within a NaviProvider"); } return context; } function NaviEmbed({ className, style, onActivityCompleted, onSessionReady, onSessionCompleted, onSessionError, onIframeClose, // Legacy event handlers onReady, onCompleted, onError, onClose, ...renderOptions }) { // Add ref to track if rendering is in progress to prevent race conditions const isRenderingRef = useRef(false); const { publishableKey, branding, loading, initialized, error: providerError, navi, } = useNavi(); const containerRef = useRef(null); const [instance, setInstance] = useState(null); const [embedError, setEmbedError] = useState(null); const [isEmbedLoading, setIsEmbedLoading] = useState(false); useEffect(() => { if (isRenderingRef.current || !navi || !containerRef.current) { return; } isRenderingRef.current = true; if (loading || providerError || !publishableKey || !initialized) { return; } async function renderEmbed() { // Prevent race conditions from React StrictMode or rapid re-renders try { if (isEmbedLoading) { console.debug("๐Ÿ” Embed is already loading, skipping duplicate"); return; } setIsEmbedLoading(true); setEmbedError(null); // Check if Navi is already loaded (should be loaded via loadNavi at app level) if (!navi) { throw new Error("Navi SDK not loaded. Please call loadNavi() at your app root before using NaviEmbed components."); } if (!containerRef.current) { throw new Error("Container ref not available"); } // If we already have an instance, don't create another if (instance) { console.log("๐Ÿ” Instance already exists, skipping creation"); return; } // Merge branding from provider with component-specific branding const mergedBranding = { ...branding, ...renderOptions.branding, }; console.log("๐ŸŽจ Applying branding:", { hasProviderBranding: Object.keys(branding).length > 0, hasComponentBranding: renderOptions.branding ? Object.keys(renderOptions.branding).length > 0 : false, brandingKeys: Object.keys(mergedBranding), primaryColor: mergedBranding.primary, }); const embedInstance = await navi.render(`#navi-embed-container`, { ...renderOptions, branding: mergedBranding, }); // Set up event listeners using centralized event types // Activity events if (onActivityCompleted) { embedInstance.on("activity-complete", onActivityCompleted); } // Session events if (onSessionReady) { embedInstance.on("navi.session.ready", onSessionReady); } if (onSessionCompleted) { embedInstance.on("navi.session.completed", onSessionCompleted); } if (onSessionError) { embedInstance.on("navi.session.error", onSessionError); } if (onIframeClose) { embedInstance.on("navi.iframe.close", onIframeClose); } // Legacy event handlers (for backward compatibility) if (onReady) { embedInstance.on("navi.session.ready", onReady); } if (onCompleted) { embedInstance.on("navi.session.completed", onCompleted); } if (onError) { embedInstance.on("navi.session.error", (event) => { onError({ message: event.data?.error || "Unknown error" }); }); } if (onClose) { embedInstance.on("navi.iframe.close", onClose); } setInstance(embedInstance); console.log("โœ… NaviEmbed instance created:", embedInstance.instanceId); } catch (err) { console.error("โŒ Failed to render embed:", err); const errorMessage = err instanceof Error ? err.message : "Failed to render embed"; setEmbedError(errorMessage); // Notify both new and legacy error handlers onSessionError?.({ type: "navi.session.error", instanceId: "unknown", data: { error: errorMessage }, timestamp: Date.now(), }); onError?.({ message: errorMessage }); } finally { setIsEmbedLoading(false); isRenderingRef.current = false; } } void renderEmbed(); // Cleanup function return () => { if (instance) { console.log("๐Ÿงน Cleaning up NaviEmbed instance:", instance.instanceId); instance.destroy(); setInstance(null); } isRenderingRef.current = false; }; }, [ publishableKey, initialized, loading, providerError, // Simplified dependencies - only track essential changes renderOptions.careflowDefinitionId, renderOptions.careflowId, renderOptions.stakeholderId, // Don't stringify branding - causes unnecessary re-renders ]); if (loading || !initialized) { return jsx("div", { id: "navi-embed-container-loading" }); } // Show error state if (providerError || embedError) { return (jsxs("div", { className: className, style: { margin: "auto", padding: "2rem", background: branding.destructive ?? "#fef2f2", borderRadius: "8px", color: branding.destructiveForeground ?? "#dc2626", textAlign: "center", ...style, }, children: [jsx("p", { children: jsx("strong", { children: "There was a problem while loading your care journey:" }) }), jsx("p", { children: providerError || embedError }), jsx("button", { onClick: () => window.location.reload(), style: { marginTop: "1rem", padding: "0.5rem 1rem", background: branding.primary ?? "#667eea", color: branding.primaryForeground ?? "white", border: "none", borderRadius: branding.radius ?? "8px", cursor: "pointer", }, children: "Retry" })] })); } return (jsx("div", { id: "navi-embed-container", ref: containerRef, className: className, style: { minHeight: "500px", ...style } })); } export { NaviEmbed, NaviProvider, useNavi };