@awell-health/navi-js
Version:
Navi.js loading utility - loads the Navi SDK from CDN
186 lines (183 loc) • 6.24 kB
JavaScript
// 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 };