<!-- One explicit-render script per session; mounts share its load promise so
concurrent instances can't clobber each other's onload. -->
<script module lang="ts">
// turnstile ships no typings; this is the surface we use
declare global {
interface Window {
turnstile?: {
render: (
container: HTMLElement,
options: {
sitekey: string;
size?: "normal" | "compact" | "flexible";
theme?: "light" | "dark" | "auto";
callback: (token: string) => void;
"expired-callback"?: () => void;
"error-callback"?: () => void;
}
) => string;
reset: (widgetId: string) => void;
remove: (widgetId: string) => void;
};
}
}
let script: HTMLScriptElement | undefined;
let loadPromise: Promise<void> | undefined;
const ensureLoaded = (): Promise<void> => {
if (window.turnstile) return Promise.resolve();
if (loadPromise) return loadPromise;
script = document.createElement("script");
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
script.async = true;
loadPromise = new Promise<void>((resolve, reject) => {
script!.addEventListener("load", () => resolve());
script!.addEventListener("error", () =>
reject(new Error("turnstile script failed to load"))
);
});
document.head.appendChild(script);
return loadPromise;
};
</script>
<script lang="ts">
import { onMount } from "svelte";
interface Props {
siteKey: string;
onToken: (token: string) => void;
/** called when an issued token expires or the challenge errors — the holder must drop it */
onExpire?: () => void;
}
let { siteKey, onToken, onExpire }: Props = $props();
let container = $state<HTMLDivElement>();
let widgetId: string | undefined;
let failed = $state(false);
export const reset = () => {
if (widgetId) window.turnstile?.reset(widgetId);
};
onMount(() => {
let cancelled = false;
ensureLoaded()
.then(() => {
if (cancelled) return;
const el = container;
const api = window.turnstile;
if (!el || !api) return;
const dark = getComputedStyle(el).colorScheme === "dark";
try {
widgetId = api.render(el, {
sitekey: siteKey,
size: "flexible",
theme: dark ? "dark" : "light",
callback: onToken,
"expired-callback": () => onExpire?.(),
"error-callback": () => onExpire?.()
});
} catch {
failed = true;
}
})
.catch(() => {
if (!cancelled) failed = true;
});
return () => {
cancelled = true;
if (widgetId) window.turnstile?.remove(widgetId);
};
});
</script>
{#if failed}
<p class="typography-paragraph-small text-foreground-danger" role="alert">
Unable to load the verification check. Check your connection and reload the page.
</p>
{:else}
<div bind:this={container} class="min-h-[65px] w-full leading-none"></div>
{/if}