UNPKG

@awell-health/navi-js

Version:

Navi.js loading utility - loads the Navi SDK from CDN

186 lines (183 loc) 6.24 kB
// CDN configuration - GCP Cloud CDN const getCDNConfig = (options) => { let origin = "https://cdn.awellhealth.com"; let embedOrigin = "https://navi-portal.awellhealth.com"; // Apply environment variables if (typeof process !== "undefined" && process.env.EMBED_ORIGIN !== undefined) { embedOrigin = process.env.EMBED_ORIGIN; } if (typeof process !== "undefined" && process.env.ORIGIN !== undefined) { origin = process.env.ORIGIN; } // Apply explicit options (highest priority) if (options?.origin) { origin = options.origin; } if (options?.embedOrigin) { embedOrigin = options.embedOrigin; } return { origin, embedOrigin, }; }; // Development: alpha version for testing // Production: will use versioned URLs later const getNaviJSUrl = (options) => { const config = getCDNConfig(options); return `${config.origin}/beta/navi.js`; }; // Updated regex patterns for GCP CDN const PRODUCTION_CDN_REGEX = /^https:\/\/cdn\.awellhealth\.com\/(alpha|beta|v\d+.*\/)?navi\.js(\?.*)?$/; const LOCALHOST_REGEX = /^http:\/\/localhost:3000\/(v1\/)?navi\.js(\?.*)?$/; const isNaviJSURL = (url) => PRODUCTION_CDN_REGEX.test(url) || LOCALHOST_REGEX.test(url); const findScript = () => { const scripts = document.querySelectorAll('script[src*="navi.js"]'); for (let i = 0; i < scripts.length; i++) { const script = scripts[i]; if (!isNaviJSURL(script.src)) { continue; } return script; } return null; }; const injectScript = (options) => { const script = document.createElement("script"); script.src = getNaviJSUrl(options); const headOrBody = document.head || document.body; if (!headOrBody) { throw new Error("Expected document.body not to be null. Navi.js requires a <body> element."); } headOrBody.appendChild(script); return script; }; let naviPromise$1 = null; let onErrorListener = null; let onLoadListener = null; const onError = (reject, scriptUrl) => (event) => { const errorDetails = event ? `Script load error for ${scriptUrl}. Check network connection and URL.` : `Unknown error loading ${scriptUrl}`; reject(new Error(`Failed to load Navi.js: ${errorDetails}`)); }; const onLoad = (resolve, reject) => () => { if (window.Navi) { resolve(window.Navi); } else { reject(new Error("Navi.js not available")); } }; const loadScript = (options) => { naviPromise$1 = new Promise((resolve, reject) => { if (typeof window === "undefined" || typeof document === "undefined") { // Resolve to null when imported server side. This makes the module // safe to import in an isomorphic code base. resolve(null); return; } try { let script = findScript(); const desiredUrl = getNaviJSUrl(options); // If a script already exists and its src matches desired, reuse it if (script && script.src === desiredUrl && !options?.alwaysFetch) { if (window.Navi) { resolve(window.Navi); return; } } else { // No script, or src differs, or forced fetch → inject fresh script if (script) { script.parentNode?.removeChild(script); } script = injectScript(options); } onLoadListener = onLoad(resolve, reject); onErrorListener = onError(reject, script.src); script.addEventListener("load", onLoadListener); script.addEventListener("error", onErrorListener); } catch (error) { reject(error); return; } }); // Resets naviPromise on error return naviPromise$1.catch((error) => { naviPromise$1 = null; return Promise.reject(error); }); }; const initNavi = (maybeNavi, args, // publishableKey startTime, options) => { if (maybeNavi === null) { return null; } const publishableKey = args[0]; const isTestKey = publishableKey.match(/^pk_test/); if (isTestKey && options?.verbose) { console.log(`🚀 Navi.js loaded using test key: ${publishableKey}`); } // Convert navi-js options to navi.js options format const naviJsOptions = options ? { ...getCDNConfig(options), verbose: options.verbose, alwaysFetch: options.alwaysFetch, } : undefined; const navi = maybeNavi(publishableKey, naviJsOptions); return navi; }; let naviPromise; let loadCalled = false; const getNaviPromise = (options) => { if (naviPromise) { if (options?.verbose) { console.log("🔍 navi-js: Navi already loaded"); } return naviPromise; } if (options?.verbose) { console.log("🔍 navi-js: loading Navi with options:", options); } naviPromise = loadScript(options).catch((error) => { // clear cache on error naviPromise = null; return Promise.reject(error); }); return naviPromise; }; const shouldAutoPreload = () => { // Env-based switch for CI/builds if (process.env.NAVI_DISABLE_PRELOAD === "true") return false; // Runtime dev flags: if your team is overriding loader behavior, skip preload // e.g. set this in your dev HTML before the app boots // <script>window.__NAVI_DISABLE_PRELOAD = true</script> if (typeof window !== "undefined" && window.__NAVI_DISABLE_PRELOAD) { return false; } return true; }; if (shouldAutoPreload()) { Promise.resolve() .then(() => getNaviPromise()) .catch((error) => { if (!loadCalled) { console.warn(error); } }); } const loadNavi = (publishableKey, options) => { loadCalled = true; const startTime = Date.now(); // if previous attempts are unsuccessful, will re-load script return getNaviPromise(options).then((maybeNavi) => { return initNavi(maybeNavi, [publishableKey], startTime, options); }); }; export { loadNavi };