@rozie-ui/captcha-react 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,353 @@
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
2
+ import { clsx, useControllableState } from "@rozie/runtime-react";
3
+ import { Fragment, jsx } from "react/jsx-runtime";
4
+ //#region src/internal/loadCaptchaApi.ts
5
+ /**
6
+ * Bridge Friendly Captcha's `createWidget` SDK (the `@friendlycaptcha/sdk`
7
+ * compat build, loaded from the CDN — NO npm peer dependency) onto `CaptchaApi`.
8
+ *
9
+ * FC has no explicit-`render`/widget-id model: `createWidget({ element, ... })`
10
+ * returns an event-emitter handle (`frc:widget.complete|expire|error`) with
11
+ * `reset()` / `start()` / `getResponse()` / `destroy()`. We treat that handle AS
12
+ * the opaque widget id the rest of the family already threads around. FC also
13
+ * has no `size` concept — `startMode` is the closest analog and rides through
14
+ * the `options` escape hatch (or a `startMode` config key if a consumer passes
15
+ * one directly).
16
+ */
17
+ const adaptFriendly = (g) => {
18
+ const fc = g;
19
+ return {
20
+ render(el, cfg) {
21
+ const h = fc.createWidget({
22
+ element: el,
23
+ sitekey: cfg.sitekey,
24
+ theme: cfg.theme,
25
+ startMode: cfg.startMode
26
+ });
27
+ h.addEventListener("frc:widget.complete", (e) => {
28
+ const cb = cfg.callback;
29
+ if (typeof cb === "function") cb(e.detail?.response ?? "");
30
+ });
31
+ h.addEventListener("frc:widget.expire", () => {
32
+ const cb = cfg["expired-callback"];
33
+ if (typeof cb === "function") cb();
34
+ });
35
+ h.addEventListener("frc:widget.error", () => {
36
+ const cb = cfg["error-callback"];
37
+ if (typeof cb === "function") cb();
38
+ });
39
+ return h;
40
+ },
41
+ reset: (id) => id.reset(),
42
+ execute: (id) => id.start(),
43
+ getResponse: (id) => id.getResponse(),
44
+ remove: (id) => id.destroy()
45
+ };
46
+ };
47
+ const CAPTCHA_PROVIDERS = {
48
+ recaptcha: {
49
+ src: "https://www.google.com/recaptcha/api.js?render=explicit",
50
+ global: "grecaptcha"
51
+ },
52
+ hcaptcha: {
53
+ src: "https://js.hcaptcha.com/1/api.js?render=explicit",
54
+ global: "hcaptcha"
55
+ },
56
+ turnstile: {
57
+ src: "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit",
58
+ global: "turnstile"
59
+ },
60
+ friendly: {
61
+ src: "https://cdn.jsdelivr.net/npm/@friendlycaptcha/sdk@1/site.compat.min.js",
62
+ global: "frcaptcha",
63
+ adapt: adaptFriendly
64
+ }
65
+ };
66
+ /**
67
+ * Load a provider's api.js ONCE across the whole document, shared by every
68
+ * `<Captcha>` instance via a `globalThis` singleton (a per-instance cache would
69
+ * re-inject per component). Resolves with the provider global once its
70
+ * `render()` is callable; rejects on an unknown provider, a script load error,
71
+ * or a `LOAD_TIMEOUT_MS` timeout. `providers` is injectable for tests.
72
+ */
73
+ function loadCaptchaApi(provider, providers = CAPTCHA_PROVIDERS) {
74
+ const cfg = providers[provider];
75
+ if (!cfg) return Promise.reject(/* @__PURE__ */ new Error("Unknown captcha provider: " + provider));
76
+ const root = globalThis;
77
+ const cache = root.__rozieCaptchaLoaders ||= {};
78
+ if (cache[provider]) return cache[provider];
79
+ const win = globalThis;
80
+ const ready = (g) => {
81
+ if (!g) return false;
82
+ const o = g;
83
+ return typeof o.render === "function" || typeof o.createWidget === "function";
84
+ };
85
+ const resolveApi = (g) => cfg.adapt ? cfg.adapt(g) : g;
86
+ const p = new Promise((resolve, reject) => {
87
+ if (ready(win[cfg.global])) {
88
+ resolve(resolveApi(win[cfg.global]));
89
+ return;
90
+ }
91
+ if (!document.querySelector("script[data-rozie-captcha=\"" + provider + "\"]")) {
92
+ const el = document.createElement("script");
93
+ el.src = cfg.src;
94
+ el.async = true;
95
+ el.defer = true;
96
+ el.setAttribute("data-rozie-captcha", provider);
97
+ el.addEventListener("error", () => reject(/* @__PURE__ */ new Error("Failed to load " + provider + " script")));
98
+ document.head.appendChild(el);
99
+ }
100
+ const started = Date.now();
101
+ const poll = setInterval(() => {
102
+ if (ready(win[cfg.global])) {
103
+ clearInterval(poll);
104
+ resolve(resolveApi(win[cfg.global]));
105
+ } else if (Date.now() - started > 2e4) {
106
+ clearInterval(poll);
107
+ reject(/* @__PURE__ */ new Error(provider + " script load timeout"));
108
+ }
109
+ }, 50);
110
+ });
111
+ cache[provider] = p;
112
+ return p;
113
+ }
114
+ //#endregion
115
+ //#region src/Captcha.tsx
116
+ const Captcha = forwardRef(function Captcha(_props, ref) {
117
+ const __defaultOptions = useState(() => ({}))[0];
118
+ const props = {
119
+ ..._props,
120
+ provider: _props.provider ?? "recaptcha",
121
+ theme: _props.theme ?? "light",
122
+ size: _props.size ?? "normal",
123
+ tabindex: _props.tabindex ?? null,
124
+ options: _props.options ?? __defaultOptions
125
+ };
126
+ const attrs = (() => {
127
+ const { provider, sitekey, token, theme, size, tabindex, options, defaultValue, onTokenChange, defaultToken, ...rest } = _props;
128
+ return rest;
129
+ })();
130
+ const disposed = useRef(false);
131
+ const api = useRef(null);
132
+ const widgetId = useRef(null);
133
+ const [token, setToken] = useControllableState({
134
+ value: props.token,
135
+ defaultValue: props.defaultToken ?? "",
136
+ onValueChange: props.onTokenChange
137
+ });
138
+ const widgetEl = useRef(null);
139
+ const { onError: _rozieProp_onError, onExpire: _rozieProp_onExpire, onVerify: _rozieProp_onVerify } = props;
140
+ const buildConfig = useCallback(() => ({
141
+ sitekey: props.sitekey,
142
+ theme: props.theme,
143
+ size: props.size,
144
+ ...props.tabindex != null ? { tabindex: props.tabindex } : {},
145
+ callback: (response) => {
146
+ setToken(response);
147
+ _rozieProp_onVerify && _rozieProp_onVerify({
148
+ token: response,
149
+ provider: props.provider
150
+ });
151
+ },
152
+ "expired-callback": () => {
153
+ setToken("");
154
+ _rozieProp_onExpire && _rozieProp_onExpire({ provider: props.provider });
155
+ },
156
+ "error-callback": () => {
157
+ setToken("");
158
+ _rozieProp_onError && _rozieProp_onError({ provider: props.provider });
159
+ },
160
+ ...props.options
161
+ }), [
162
+ _rozieProp_onError,
163
+ _rozieProp_onExpire,
164
+ _rozieProp_onVerify,
165
+ props.options,
166
+ props.provider,
167
+ props.sitekey,
168
+ props.size,
169
+ props.tabindex,
170
+ props.theme,
171
+ setToken
172
+ ]);
173
+ function reset() {
174
+ if (widgetId.current != null && api.current && typeof api.current.reset === "function") api.current.reset(widgetId.current);
175
+ setToken("");
176
+ }
177
+ function execute() {
178
+ if (widgetId.current != null && api.current && typeof api.current.execute === "function") api.current.execute(widgetId.current);
179
+ }
180
+ function getResponse() {
181
+ return widgetId.current != null && api.current && typeof api.current.getResponse === "function" ? api.current.getResponse(widgetId.current) : "";
182
+ }
183
+ useEffect(() => {
184
+ disposed.current = false;
185
+ loadCaptchaApi(props.provider).then((a) => {
186
+ if (disposed.current) return;
187
+ api.current = a;
188
+ widgetId.current = api.current.render(widgetEl.current, buildConfig());
189
+ }).catch((err) => {
190
+ props.onError && props.onError({
191
+ provider: props.provider,
192
+ error: err
193
+ });
194
+ });
195
+ return () => {
196
+ disposed.current = true;
197
+ if (widgetId.current == null || !api.current) return;
198
+ if (typeof api.current.remove === "function") api.current.remove(widgetId.current);
199
+ else if (typeof api.current.reset === "function") api.current.reset(widgetId.current);
200
+ };
201
+ }, []);
202
+ const _rozieExposeRef = useRef({
203
+ reset,
204
+ execute,
205
+ getResponse
206
+ });
207
+ _rozieExposeRef.current = {
208
+ reset,
209
+ execute,
210
+ getResponse
211
+ };
212
+ useImperativeHandle(ref, () => ({
213
+ reset: (...args) => _rozieExposeRef.current.reset(...args),
214
+ execute: (...args) => _rozieExposeRef.current.execute(...args),
215
+ getResponse: (...args) => _rozieExposeRef.current.getResponse(...args)
216
+ }), []);
217
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", {
218
+ ref: widgetEl,
219
+ ...attrs,
220
+ className: clsx("rozie-captcha", attrs.className),
221
+ "data-rozie-s-9c7749d4": ""
222
+ }) });
223
+ });
224
+ //#endregion
225
+ //#region src/internal/loadRecaptchaV3.ts
226
+ /** The v3 api.js base URL. The sitekey is appended (`render=SITEKEY`) per call. */
227
+ const RECAPTCHA_V3_SRC = "https://www.google.com/recaptcha/api.js?render=";
228
+ const getGrecaptcha = () => globalThis.grecaptcha;
229
+ /**
230
+ * Load reCAPTCHA v3's api.js ONCE per sitekey across the whole document, shared
231
+ * by every `<RecaptchaV3>` instance via a `globalThis` singleton keyed on the
232
+ * sitekey. Resolves with the `grecaptcha` global once `grecaptcha.ready(cb)` has
233
+ * fired; rejects on a script load error or a `LOAD_TIMEOUT_MS` timeout.
234
+ *
235
+ * `srcBase` is injectable for tests (pass `''` so happy-dom does not attempt a
236
+ * real network fetch — mirrors loadCaptchaApi's injectable `providers` seam).
237
+ */
238
+ function loadRecaptchaV3(sitekey, srcBase = RECAPTCHA_V3_SRC) {
239
+ const root = globalThis;
240
+ const cache = root.__rozieRecaptchaV3Loaders ||= {};
241
+ if (cache[sitekey]) return cache[sitekey];
242
+ const p = new Promise((resolve, reject) => {
243
+ let settled = false;
244
+ const ready = (g) => {
245
+ g.ready(() => {
246
+ if (settled) return;
247
+ settled = true;
248
+ resolve(g);
249
+ });
250
+ };
251
+ const present = getGrecaptcha();
252
+ if (present && typeof present.ready === "function") {
253
+ ready(present);
254
+ return;
255
+ }
256
+ if (!document.querySelector("script[data-rozie-recaptcha-v3=\"" + sitekey + "\"]")) {
257
+ const el = document.createElement("script");
258
+ el.src = srcBase + encodeURIComponent(sitekey);
259
+ el.async = true;
260
+ el.defer = true;
261
+ el.setAttribute("data-rozie-recaptcha-v3", sitekey);
262
+ el.addEventListener("error", () => {
263
+ if (settled) return;
264
+ settled = true;
265
+ reject(/* @__PURE__ */ new Error("Failed to load reCAPTCHA v3 script for sitekey: " + sitekey));
266
+ });
267
+ document.head.appendChild(el);
268
+ }
269
+ const started = Date.now();
270
+ const poll = setInterval(() => {
271
+ const g = getGrecaptcha();
272
+ if (g && typeof g.ready === "function") {
273
+ clearInterval(poll);
274
+ ready(g);
275
+ } else if (Date.now() - started > 2e4) {
276
+ clearInterval(poll);
277
+ if (settled) return;
278
+ settled = true;
279
+ reject(/* @__PURE__ */ new Error("reCAPTCHA v3 script load timeout for sitekey: " + sitekey));
280
+ }
281
+ }, 50);
282
+ });
283
+ cache[sitekey] = p;
284
+ return p;
285
+ }
286
+ /**
287
+ * Run a v3 challenge for `sitekey` + `action`, returning a fresh token.
288
+ * Delegates to `grecaptcha.execute(sitekey, { action })`. Call AFTER
289
+ * `loadRecaptchaV3(sitekey)` has resolved (the component threads that order).
290
+ */
291
+ function execute(sitekey, opts) {
292
+ const g = getGrecaptcha();
293
+ if (!g || typeof g.execute !== "function") return Promise.reject(/* @__PURE__ */ new Error("reCAPTCHA v3 not loaded for sitekey: " + sitekey));
294
+ return g.execute(sitekey, opts);
295
+ }
296
+ //#endregion
297
+ //#region src/RecaptchaV3.tsx
298
+ const RecaptchaV3 = forwardRef(function RecaptchaV3(_props, ref) {
299
+ const props = {
300
+ ..._props,
301
+ action: _props.action ?? "submit",
302
+ executeOnMount: _props.executeOnMount ?? false
303
+ };
304
+ const attrs = (() => {
305
+ const { sitekey, action, token, executeOnMount, defaultValue, onTokenChange, defaultToken, ...rest } = _props;
306
+ return rest;
307
+ })();
308
+ const disposed = useRef(false);
309
+ const [token, setToken] = useControllableState({
310
+ value: props.token,
311
+ defaultValue: props.defaultToken ?? "",
312
+ onValueChange: props.onTokenChange
313
+ });
314
+ function execute$1(action = null) {
315
+ const a = action != null ? action : props.action;
316
+ return loadRecaptchaV3(props.sitekey).then(() => execute(props.sitekey, { action: a })).then((tok) => {
317
+ if (disposed.current) return tok;
318
+ setToken(tok);
319
+ props.onVerify && props.onVerify({
320
+ token: tok,
321
+ action: a
322
+ });
323
+ return tok;
324
+ }).catch((err) => {
325
+ if (!disposed.current) props.onError && props.onError({ error: err });
326
+ throw err;
327
+ });
328
+ }
329
+ useEffect(() => {
330
+ disposed.current = false;
331
+ loadRecaptchaV3(props.sitekey).then(() => {
332
+ if (disposed.current || !props.executeOnMount) return;
333
+ execute$1();
334
+ }).catch((err) => {
335
+ if (disposed.current) return;
336
+ props.onError && props.onError({ error: err });
337
+ });
338
+ return () => {
339
+ disposed.current = true;
340
+ };
341
+ }, []);
342
+ const _rozieExposeRef = useRef({ execute: execute$1 });
343
+ _rozieExposeRef.current = { execute: execute$1 };
344
+ useImperativeHandle(ref, () => ({ execute: (...args) => _rozieExposeRef.current.execute(...args) }), []);
345
+ return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", {
346
+ style: { display: "none" },
347
+ ...attrs,
348
+ className: clsx("rozie-recaptcha-v3", attrs.className),
349
+ "data-rozie-s-9148a0b0": ""
350
+ }) });
351
+ });
352
+ //#endregion
353
+ export { Captcha, Captcha as default, RecaptchaV3 };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@rozie-ui/captcha-react",
3
+ "version": "0.1.3",
4
+ "type": "module",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "description": "Idiomatic React Captcha — one Rozie source compiled to React.",
8
+ "keywords": [
9
+ "rozie",
10
+ "rozie-ui",
11
+ "captcha",
12
+ "recaptcha",
13
+ "hcaptcha",
14
+ "turnstile",
15
+ "bot-protection",
16
+ "verification",
17
+ "spam",
18
+ "headless",
19
+ "component",
20
+ "react"
21
+ ],
22
+ "author": "One Learning Community (https://github.com/One-Learning-Community)",
23
+ "homepage": "https://github.com/One-Learning-Community/rozie.js#readme",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/One-Learning-Community/rozie.js.git",
27
+ "directory": "packages/ui/captcha/packages/react"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/One-Learning-Community/rozie.js/issues"
31
+ },
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.mjs",
34
+ "types": "./dist/index.d.mts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.mts",
38
+ "import": "./dist/index.mjs",
39
+ "require": "./dist/index.cjs"
40
+ },
41
+ "./source": "./src/Captcha.tsx"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "src"
46
+ ],
47
+ "dependencies": {
48
+ "@rozie/runtime-react": "0.1.1"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^18.2 || ^19",
52
+ "react-dom": "^18.2 || ^19"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "react": {
56
+ "optional": false
57
+ },
58
+ "react-dom": {
59
+ "optional": false
60
+ }
61
+ },
62
+ "devDependencies": {
63
+ "@types/react": "^18",
64
+ "@types/react-dom": "^18"
65
+ },
66
+ "publishConfig": {
67
+ "access": "public"
68
+ },
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "typecheck": "tsc --noEmit"
72
+ }
73
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ForwardRefExoticComponent, RefAttributes } from 'react';
3
+ import type * as React from 'react';
4
+
5
+ export interface CaptchaProps {
6
+ provider?: string;
7
+ sitekey: string;
8
+ token?: string;
9
+ defaultToken?: string;
10
+ onTokenChange?: (next: string) => void;
11
+ theme?: string;
12
+ size?: string;
13
+ tabindex?: (number) | null;
14
+ options?: Record<string, unknown>;
15
+ onVerify?: (...args: unknown[]) => void;
16
+ onExpire?: (...args: unknown[]) => void;
17
+ onError?: (...args: unknown[]) => void;
18
+ }
19
+
20
+ export interface CaptchaHandle {
21
+ reset: (...args: any[]) => any;
22
+ execute: (...args: any[]) => any;
23
+ getResponse: (...args: any[]) => any;
24
+ }
25
+
26
+ declare const Captcha: React.ForwardRefExoticComponent<CaptchaProps & React.RefAttributes<CaptchaHandle>>;
27
+ export default Captcha;
@@ -0,0 +1,141 @@
1
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import { clsx, useControllableState } from '@rozie/runtime-react';
3
+ // The provider api.js loader (inject-once singleton + poll/timeout/error) lives
4
+ // in a vendored internal module so its branchy logic is unit-tested independent
5
+ // of any framework (see internal/loadCaptchaApi.test.ts). codegen copies
6
+ // src/internal/ into every leaf, so this relative import resolves verbatim ×6.
7
+ import { loadCaptchaApi } from './internal/loadCaptchaApi';
8
+
9
+ // Live widget handle. Top-level lets → React hoists to useRef (setup-once).
10
+ // `disposed` MUST be top-level (not declared inside $onMount): the Solid emitter
11
+ // extracts the teardown into a separate onCleanup() whose scope can't see a
12
+ // mount-body local, so a `let disposed` inside $onMount is out of scope in the
13
+ // teardown (TS2304). Top-level — like api/widgetId — is visible to both.
14
+
15
+ interface CaptchaProps {
16
+ provider?: string;
17
+ sitekey: string;
18
+ token?: string;
19
+ defaultToken?: string;
20
+ onTokenChange?: (token: string) => void;
21
+ theme?: string;
22
+ size?: string;
23
+ tabindex?: (number) | null;
24
+ options?: Record<string, any>;
25
+ onVerify?: (...args: any[]) => void;
26
+ onExpire?: (...args: any[]) => void;
27
+ onError?: (...args: any[]) => void;
28
+ }
29
+
30
+ export interface CaptchaHandle {
31
+ reset: (...args: any[]) => any;
32
+ execute: (...args: any[]) => any;
33
+ getResponse: (...args: any[]) => any;
34
+ }
35
+
36
+ const Captcha = forwardRef<CaptchaHandle, CaptchaProps>(function Captcha(_props: CaptchaProps, ref): JSX.Element {
37
+ const __defaultOptions = useState(() => (() => ({}))())[0];
38
+ const props: Omit<CaptchaProps, 'provider' | 'theme' | 'size' | 'tabindex' | 'options'> & { provider: string; theme: string; size: string; tabindex: (number) | null; options: Record<string, any> } = {
39
+ ..._props,
40
+ provider: _props.provider ?? 'recaptcha',
41
+ theme: _props.theme ?? 'light',
42
+ size: _props.size ?? 'normal',
43
+ tabindex: _props.tabindex ?? null,
44
+ options: _props.options ?? __defaultOptions,
45
+ };
46
+ const attrs: Record<string, unknown> = (() => {
47
+ const { provider, sitekey, token, theme, size, tabindex, options, defaultValue, onTokenChange, defaultToken, ...rest } = _props as CaptchaProps & Record<string, unknown>;
48
+ void provider; void sitekey; void token; void theme; void size; void tabindex; void options; void defaultValue; void onTokenChange; void defaultToken;
49
+ return rest;
50
+ })();
51
+ const disposed = useRef(false);
52
+ const api = useRef<any>(null);
53
+ const widgetId = useRef<any>(null);
54
+ const [token, setToken] = useControllableState({
55
+ value: props.token,
56
+ defaultValue: props.defaultToken ?? '',
57
+ onValueChange: props.onTokenChange,
58
+ });
59
+ const widgetEl = useRef<HTMLDivElement | null>(null);
60
+
61
+ const { onError: _rozieProp_onError, onExpire: _rozieProp_onExpire, onVerify: _rozieProp_onVerify } = props;
62
+ const buildConfig = useCallback(() => ({
63
+ sitekey: props.sitekey,
64
+ theme: props.theme,
65
+ size: props.size,
66
+ ...(props.tabindex != null ? {
67
+ tabindex: props.tabindex
68
+ } : {}),
69
+ // NB: the param must NOT be named `token` — on Vue, $model.token lowers to a
70
+ // `defineModel('token')` ref named `token`, and a same-named param shadows it
71
+ // (`token.value = token` would write the param, not the model → v-model:token
72
+ // never populates). Vue-only footgun (React/Solid lower to a setToken call).
73
+ callback: (response: any) => {
74
+ setToken(response);
75
+ _rozieProp_onVerify && _rozieProp_onVerify({
76
+ token: response,
77
+ provider: props.provider
78
+ });
79
+ },
80
+ 'expired-callback': () => {
81
+ setToken('');
82
+ _rozieProp_onExpire && _rozieProp_onExpire({
83
+ provider: props.provider
84
+ });
85
+ },
86
+ 'error-callback': () => {
87
+ setToken('');
88
+ _rozieProp_onError && _rozieProp_onError({
89
+ provider: props.provider
90
+ });
91
+ },
92
+ ...props.options
93
+ }), [_rozieProp_onError, _rozieProp_onExpire, _rozieProp_onVerify, props.options, props.provider, props.sitekey, props.size, props.tabindex, props.theme, setToken]);
94
+ // Imperative handle. Each guards on a live widget (null before render / after
95
+ // teardown). reset clears the two-way token to match the cleared widget.
96
+ function reset() {
97
+ if (widgetId.current != null && api.current && typeof api.current.reset === 'function') api.current.reset(widgetId.current);
98
+ setToken('');
99
+ }
100
+ // Invisible / programmatic challenge (size="invisible"). No-op until rendered.
101
+ // Invisible / programmatic challenge (size="invisible"). No-op until rendered.
102
+ function execute() {
103
+ if (widgetId.current != null && api.current && typeof api.current.execute === 'function') api.current.execute(widgetId.current);
104
+ }
105
+ // Read the current response token on demand (e.g. just before form submit).
106
+ // Read the current response token on demand (e.g. just before form submit).
107
+ function getResponse() {
108
+ return widgetId.current != null && api.current && typeof api.current.getResponse === 'function' ? api.current.getResponse(widgetId.current) : '';
109
+ }
110
+
111
+ useEffect(() => {
112
+ disposed.current = false;
113
+ loadCaptchaApi(props.provider).then((a: any) => {
114
+ if (disposed.current) return;
115
+ api.current = a;
116
+ widgetId.current = api.current.render(widgetEl.current!, buildConfig());
117
+ }).catch((err: any) => {
118
+ props.onError && props.onError({
119
+ provider: props.provider,
120
+ error: err
121
+ });
122
+ });
123
+ return () => {
124
+ disposed.current = true;
125
+ if (widgetId.current == null || !api.current) return;
126
+ // Turnstile fully removes a widget; reCAPTCHA/hCaptcha only reset.
127
+ if (typeof api.current.remove === 'function') api.current.remove(widgetId.current);else if (typeof api.current.reset === 'function') api.current.reset(widgetId.current);
128
+ };
129
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
130
+
131
+ const _rozieExposeRef = useRef({ reset, execute, getResponse });
132
+ _rozieExposeRef.current = { reset, execute, getResponse };
133
+ useImperativeHandle(ref, () => ({ reset: (...args: Parameters<typeof reset>): ReturnType<typeof reset> => _rozieExposeRef.current.reset(...args), execute: (...args: Parameters<typeof execute>): ReturnType<typeof execute> => _rozieExposeRef.current.execute(...args), getResponse: (...args: Parameters<typeof getResponse>): ReturnType<typeof getResponse> => _rozieExposeRef.current.getResponse(...args) }), []);
134
+
135
+ return (
136
+ <>
137
+ <div ref={widgetEl} {...attrs} className={clsx("rozie-captcha", (attrs.className as string | undefined))} data-rozie-s-9c7749d4="" />
138
+ </>
139
+ );
140
+ });
141
+ export default Captcha;
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ForwardRefExoticComponent, RefAttributes } from 'react';
3
+ import type * as React from 'react';
4
+
5
+ export interface RecaptchaV3Props {
6
+ sitekey: string;
7
+ action?: string;
8
+ token?: string;
9
+ defaultToken?: string;
10
+ onTokenChange?: (next: string) => void;
11
+ executeOnMount?: boolean;
12
+ onError?: (...args: unknown[]) => void;
13
+ onVerify?: (...args: unknown[]) => void;
14
+ }
15
+
16
+ export interface RecaptchaV3Handle {
17
+ execute: (...args: any[]) => any;
18
+ }
19
+
20
+ declare const RecaptchaV3: React.ForwardRefExoticComponent<RecaptchaV3Props & React.RefAttributes<RecaptchaV3Handle>>;
21
+ export default RecaptchaV3;