@awell-health/navi-js-react
Version:
React components and hooks for integrating Navi care flows
202 lines (198 loc) โข 8.71 kB
JavaScript
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 };