Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | <script lang="ts"> import { onMount } from 'svelte'; import { browser } from '$app/environment'; import { PUBLIC_HCAPTCHA_SITE_KEY } from '$env/static/public'; interface Props { /** Called with the verified token. Token is single-use; clear on submit. */ onverify?: (token: string) => void; /** Called when the token expires before submit. */ onexpire?: () => void; /** Called on widget error (network failure, ad-blocker, hCaptcha outage). */ onerror?: () => void; } let { onverify, onexpire, onerror }: Props = $props(); // `loaded` flips to true once the dynamic import resolves and the // <h-captcha> custom element is registered. Until then we render only the // SSR-stable placeholder, so initial markup matches between server and // client and the layout doesn't shift on hydration. let loaded = $state(false); // Reference to the underlying <h-captcha> element so we can call reset() // on it when a consumer's auth call fails. let widgetEl: HTMLElement | undefined = $state(); const sitekey = PUBLIC_HCAPTCHA_SITE_KEY ?? ''; /** * Reset the widget — exposed via bind:this. Single-use tokens are consumed * by Supabase on first verification; on a failed auth call the consumer * must reset() so the user gets a fresh challenge instead of being stuck * with a dead token. */ export function reset(): void { // Underlying element exposes reset() per @hcaptcha/vanilla-hcaptcha API. (widgetEl as unknown as { reset?: () => void } | undefined)?.reset?.(); } function handleVerified(e: Event): void { const detail = (e as CustomEvent<{ token?: string }>).detail; // Older versions of the wrapper attach `token` to the event itself // rather than `event.detail`; accept both shapes. const token = detail?.token ?? (e as unknown as { token?: string }).token ?? ''; if (token) onverify?.(token); } function handleExpired(): void { onexpire?.(); } function handleError(): void { if (onerror) onerror(); } // `challenge-expired` is hyphenated and can't be wired via the on{event} // attribute shorthand; subscribe imperatively once the element is bound. $effect(() => { if (!widgetEl) return; const el = widgetEl; el.addEventListener('challenge-expired', handleExpired); return () => el.removeEventListener('challenge-expired', handleExpired); }); onMount(() => { // Web component touches `customElements`; only register client-side. // Dynamic import keeps the wrapper out of the SSR bundle entirely. if (!sitekey) { // Misconfiguration — surface via onerror so the consumer's submit // guard keeps short-circuiting. We still render nothing. onerror?.(); return; } import('@hcaptcha/vanilla-hcaptcha') .then(() => { loaded = true; }) .catch(() => { // Import failure (CDN blocked, network outage). Notify consumer // so the inline guard message stays visible and submit is gated. onerror?.(); }); }); </script> <div class="hcaptcha" role="group" aria-label="CAPTCHA challenge" data-captcha-placeholder> {#if Ibrowser && loaded && sitekey} <h-captcha bind:this={widgetEl} sitekey={sitekey} size="normal" onverified={handleVerified} onexpired={handleExpired} onerror={handleError} ></h-captcha> {/if} </div> <style> .hcaptcha { /* The widget renders a fixed 303x78 iframe; reserve space so the layout * doesn't shift when the script finishes loading. */ min-height: 78px; } </style> |