use std::cell::RefCell;
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{HtmlScriptElement, window};
const CLERK_JS_MAJOR: &str = "6";
const CLERK_UI_MAJOR: &str = "1";
const SCRIPT_ID: &str = "__dioxus_clerk_js";
fn fallback_cdn_url() -> String {
format!("https://cdn.jsdelivr.net/npm/@clerk/clerk-js@{CLERK_JS_MAJOR}/dist/clerk.browser.js")
}
fn fallback_ui_url() -> String {
format!("https://cdn.jsdelivr.net/npm/@clerk/ui@{CLERK_UI_MAJOR}/dist/ui.browser.js")
}
const UI_SCRIPT_ID: &str = "__dioxus_clerk_ui";
fn default_script_url(publishable_key: &str) -> String {
match crate::publishable_key::frontend_api_host(publishable_key) {
Some(host) => {
format!("https://{host}/npm/@clerk/clerk-js@{CLERK_JS_MAJOR}/dist/clerk.browser.js")
}
None => fallback_cdn_url(),
}
}
fn default_ui_url(publishable_key: &str) -> String {
match crate::publishable_key::frontend_api_host(publishable_key) {
Some(host) => format!("https://{host}/npm/@clerk/ui@{CLERK_UI_MAJOR}/dist/ui.browser.js"),
None => fallback_ui_url(),
}
}
thread_local! {
static SCRIPT_LOAD_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub(crate) fn script_load_error() -> Option<String> {
SCRIPT_LOAD_ERROR.with(|slot| slot.borrow().clone())
}
pub(crate) fn ui_script_injected() -> bool {
window()
.and_then(|w| w.document())
.and_then(|doc| doc.get_element_by_id(UI_SCRIPT_ID))
.is_some()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct ScriptOptions {
pub(crate) url: Option<String>,
pub(crate) ui_url: Option<String>,
pub(crate) nonce: Option<String>,
}
pub(crate) fn inject_script(publishable_key: &str, options: &ScriptOptions) -> bool {
let Some(window) = window() else { return false };
let Some(doc) = window.document() else {
return false;
};
if doc.get_element_by_id(SCRIPT_ID).is_some() || doc.get_element_by_id(UI_SCRIPT_ID).is_some() {
if script_load_error().is_none() {
return false;
}
if let Some(existing) = doc.get_element_by_id(SCRIPT_ID) {
existing.remove();
}
if let Some(existing) = doc.get_element_by_id(UI_SCRIPT_ID) {
existing.remove();
}
SCRIPT_LOAD_ERROR.with(|slot| *slot.borrow_mut() = None);
}
if crate::bindings::clerk_singleton().is_some() {
return false;
}
let ui_url = options
.ui_url
.clone()
.unwrap_or_else(|| default_ui_url(publishable_key));
let clerk_url = options
.url
.clone()
.unwrap_or_else(|| default_script_url(publishable_key));
let ui_ok = inject_one(
&doc,
UI_SCRIPT_ID,
&ui_url,
None,
options.nonce.as_deref(),
"@clerk/ui",
);
let clerk_ok = inject_one(
&doc,
SCRIPT_ID,
&clerk_url,
Some(publishable_key),
options.nonce.as_deref(),
"clerk-js",
);
ui_ok && clerk_ok
}
fn inject_one(
doc: &web_sys::Document,
id: &str,
url: &str,
publishable_key: Option<&str>,
nonce: Option<&str>,
label: &str,
) -> bool {
let Some(script) = doc
.create_element("script")
.ok()
.and_then(|element| element.dyn_into::<HtmlScriptElement>().ok())
else {
return false;
};
script.set_id(id);
script.set_src(url);
if let Some(key) = publishable_key {
script.set_attribute("data-clerk-publishable-key", key).ok();
}
if let Some(nonce) = nonce {
script.set_attribute("nonce", nonce).ok();
}
script.set_attribute("crossorigin", "anonymous").ok();
let error_url = url.to_owned();
let label = label.to_owned();
let onerror = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {
SCRIPT_LOAD_ERROR.with(|slot| {
*slot.borrow_mut() = Some(format!(
"the {label} script at {error_url} failed to load; check the network tab for DNS/CSP/offline failures"
));
});
});
script.set_onerror(Some(onerror.as_ref().unchecked_ref()));
onerror.forget();
match doc.head() {
Some(head) => head.append_child(&script).is_ok(),
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use js_sys::{Function, Object, Reflect};
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
const UNREACHABLE_URL: &str = "https://clerk.invalid/clerk.browser.js";
const UNREACHABLE_UI_URL: &str = "https://clerk.invalid/ui.browser.js";
fn cleanup() {
let window = window().expect("wasm tests run in a browser window");
if let Some(document) = window.document() {
for id in [SCRIPT_ID, UI_SCRIPT_ID] {
if let Some(script) = document.get_element_by_id(id) {
script.remove();
}
}
}
Reflect::set(
window.as_ref(),
&JsValue::from_str("Clerk"),
&JsValue::UNDEFINED,
)
.unwrap();
SCRIPT_LOAD_ERROR.with(|slot| *slot.borrow_mut() = None);
}
fn unreachable_options() -> ScriptOptions {
ScriptOptions {
url: Some(UNREACHABLE_URL.into()),
ui_url: Some(UNREACHABLE_UI_URL.into()),
nonce: None,
}
}
#[wasm_bindgen_test]
fn inject_script_adds_one_tag_per_page() {
cleanup();
assert!(inject_script("pk_test_loader", &unreachable_options()));
assert!(
!inject_script("pk_test_loader", &unreachable_options()),
"a second injection must not add another clerk-js copy"
);
cleanup();
}
#[wasm_bindgen_test]
fn inject_script_sets_source_and_publishable_key() {
cleanup();
assert!(inject_script("pk_test_loader", &unreachable_options()));
let document = window().unwrap().document().unwrap();
let script = document
.get_element_by_id(SCRIPT_ID)
.expect("script tag was injected");
assert_eq!(
script
.get_attribute("data-clerk-publishable-key")
.as_deref(),
Some("pk_test_loader")
);
assert_eq!(
script.get_attribute("src").as_deref(),
Some(UNREACHABLE_URL)
);
assert_eq!(
script.get_attribute("crossorigin").as_deref(),
Some("anonymous")
);
cleanup();
}
#[wasm_bindgen_test]
fn inject_script_adds_clerk_ui_bundle() {
cleanup();
assert!(inject_script("pk_test_loader", &unreachable_options()));
let document = window().unwrap().document().unwrap();
let ui = document
.get_element_by_id(UI_SCRIPT_ID)
.expect("the @clerk/ui script tag was injected");
assert_eq!(ui.get_attribute("src").as_deref(), Some(UNREACHABLE_UI_URL));
assert_eq!(
ui.get_attribute("crossorigin").as_deref(),
Some("anonymous")
);
assert!(ui.get_attribute("data-clerk-publishable-key").is_none());
cleanup();
}
#[wasm_bindgen_test]
fn inject_script_skips_when_clerk_global_already_exists() {
cleanup();
let clerk = Object::new();
Reflect::set(
clerk.as_ref(),
&JsValue::from_str("load"),
Function::new_no_args("return Promise.resolve();").as_ref(),
)
.unwrap();
Reflect::set(
window().unwrap().as_ref(),
&JsValue::from_str("Clerk"),
clerk.as_ref(),
)
.unwrap();
assert!(
!inject_script("pk_test_loader", &unreachable_options()),
"a second clerk-js copy would replace the live window.Clerk"
);
cleanup();
}
#[wasm_bindgen_test(async)]
async fn failed_script_load_records_fail_fast_error() {
cleanup();
assert!(inject_script("pk_test_loader", &unreachable_options()));
for _ in 0..200 {
if script_load_error().is_some() {
break;
}
gloo_timers::future::TimeoutFuture::new(25).await;
}
let message = script_load_error().expect("script error handler records a failure");
assert!(message.contains("failed to load"));
cleanup();
}
#[wasm_bindgen_test(async)]
async fn failed_script_load_allows_reinjection_retry() {
cleanup();
assert!(inject_script("pk_test_loader", &unreachable_options()));
for _ in 0..200 {
if script_load_error().is_some() {
break;
}
gloo_timers::future::TimeoutFuture::new(25).await;
}
assert!(script_load_error().is_some());
assert!(
inject_script("pk_test_loader", &unreachable_options()),
"retry after a failed load must inject a fresh tag"
);
assert!(
script_load_error().is_none(),
"retry must clear the recorded load error"
);
cleanup();
}
}