@prosopo/procaptcha-puzzle 2.13.3 → 2.14.1
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/.turbo/turbo-build$colon$cjs.log +6 -6
- package/.turbo/turbo-build$colon$tsc.log +20 -20
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +66 -0
- package/dist/cjs/components/procaptchaPuzzle.cjs +22 -0
- package/dist/cjs/components/procaptchaWidget.cjs +225 -0
- package/dist/cjs/components/puzzleCanvas.cjs +451 -0
- package/dist/cjs/index.cjs +7 -2
- package/dist/components/procaptchaPuzzle.d.ts +7 -0
- package/dist/components/procaptchaPuzzle.d.ts.map +1 -0
- package/dist/components/procaptchaPuzzle.js +21 -0
- package/dist/components/procaptchaPuzzle.js.map +1 -0
- package/dist/components/procaptchaWidget.d.ts +7 -0
- package/dist/components/procaptchaWidget.d.ts.map +1 -0
- package/dist/components/procaptchaWidget.js +225 -0
- package/dist/components/procaptchaWidget.js.map +1 -0
- package/dist/components/{PuzzleCanvas.d.ts → puzzleCanvas.d.ts} +6 -4
- package/dist/components/puzzleCanvas.d.ts.map +1 -0
- package/dist/components/puzzleCanvas.js +451 -0
- package/dist/components/puzzleCanvas.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/tests/domHarness.d.ts +20 -0
- package/dist/tests/domHarness.d.ts.map +1 -0
- package/dist/tests/domHarness.js +56 -0
- package/dist/tests/domHarness.js.map +1 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.d.ts +2 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.d.ts.map +1 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.js +102 -0
- package/dist/tests/invisibleCanvasVisibility.unit.test.js.map +1 -0
- package/dist/tests/procaptchaPuzzle.test-d.js +13 -10
- package/dist/tests/procaptchaPuzzle.test-d.js.map +1 -1
- package/dist/tests/procaptchaPuzzle.unit.test.js +44 -33
- package/dist/tests/procaptchaPuzzle.unit.test.js.map +1 -1
- package/dist/tests/procaptchaWidget.unit.test.js +108 -175
- package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
- package/dist/tests/puzzleCanvas.unit.test.js +56 -82
- package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
- package/package.json +10 -14
- package/src/components/procaptchaPuzzle.ts +54 -0
- package/src/components/procaptchaWidget.ts +392 -0
- package/src/components/puzzleCanvas.ts +658 -0
- package/src/index.ts +3 -2
- package/src/tests/domHarness.ts +122 -0
- package/src/tests/invisibleCanvasVisibility.unit.test.ts +179 -0
- package/src/tests/procaptchaPuzzle.test-d.ts +33 -17
- package/src/tests/procaptchaPuzzle.unit.test.ts +59 -44
- package/src/tests/procaptchaWidget.unit.test.ts +145 -258
- package/src/tests/puzzleCanvas.unit.test.ts +94 -122
- package/tsconfig.cjs.json +1 -2
- package/tsconfig.json +1 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +3 -4
- package/dist/cjs/components/ProcaptchaPuzzle.cjs +0 -7
- package/dist/cjs/components/ProcaptchaWidget.cjs +0 -206
- package/dist/cjs/components/PuzzleCanvas.cjs +0 -476
- package/dist/components/ProcaptchaPuzzle.d.ts +0 -3
- package/dist/components/ProcaptchaPuzzle.d.ts.map +0 -1
- package/dist/components/ProcaptchaPuzzle.js +0 -7
- package/dist/components/ProcaptchaPuzzle.js.map +0 -1
- package/dist/components/ProcaptchaWidget.d.ts +0 -4
- package/dist/components/ProcaptchaWidget.d.ts.map +0 -1
- package/dist/components/ProcaptchaWidget.js +0 -207
- package/dist/components/ProcaptchaWidget.js.map +0 -1
- package/dist/components/PuzzleCanvas.d.ts.map +0 -1
- package/dist/components/PuzzleCanvas.js +0 -476
- package/dist/components/PuzzleCanvas.js.map +0 -1
- package/dist/tests/setup.d.ts +0 -5
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -3
- package/dist/tests/setup.js.map +0 -1
- package/src/components/ProcaptchaPuzzle.tsx +0 -36
- package/src/components/ProcaptchaWidget.tsx +0 -320
- package/src/components/PuzzleCanvas.tsx +0 -638
- package/src/tests/setup.ts +0 -26
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
import { createTranslator, loadI18next } from "@prosopo/locale";
|
|
16
|
+
import {
|
|
17
|
+
type CheckboxProps,
|
|
18
|
+
type Component,
|
|
19
|
+
type HoneypotComponent,
|
|
20
|
+
type ProcaptchaStateHandle,
|
|
21
|
+
Teardown,
|
|
22
|
+
buildUpdateState,
|
|
23
|
+
createElement,
|
|
24
|
+
createProcaptchaState,
|
|
25
|
+
createRenderScheduler,
|
|
26
|
+
isEventTrusted,
|
|
27
|
+
mountCheckbox,
|
|
28
|
+
mountHoneypot,
|
|
29
|
+
} from "@prosopo/procaptcha-common";
|
|
30
|
+
import {
|
|
31
|
+
type GetPuzzleCaptchaResponse,
|
|
32
|
+
ModeEnum,
|
|
33
|
+
type ProcaptchaProps,
|
|
34
|
+
type ProcaptchaState,
|
|
35
|
+
type PuzzleEvent,
|
|
36
|
+
} from "@prosopo/types";
|
|
37
|
+
import { darkTheme, lightTheme } from "@prosopo/widget-skeleton";
|
|
38
|
+
import { Manager } from "../services/Manager.js";
|
|
39
|
+
import { type PuzzleCanvasProps, mountPuzzleCanvas } from "./puzzleCanvas.js";
|
|
40
|
+
|
|
41
|
+
// Define the same event name as in the bundle for consistency
|
|
42
|
+
const PROCAPTCHA_EXECUTE_EVENT = "procaptcha:execute";
|
|
43
|
+
|
|
44
|
+
type PuzzlePhase = "checkbox" | "dragging" | "submitting";
|
|
45
|
+
|
|
46
|
+
export interface ProcaptchaPuzzleHandle {
|
|
47
|
+
destroy(): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const mountProcaptchaPuzzleWidget = (
|
|
51
|
+
container: HTMLElement,
|
|
52
|
+
props: ProcaptchaProps,
|
|
53
|
+
): ProcaptchaPuzzleHandle => {
|
|
54
|
+
const teardown = new Teardown();
|
|
55
|
+
const config = props.config;
|
|
56
|
+
const i18n = props.i18n;
|
|
57
|
+
const frictionlessState = props.frictionlessState; // Set up Session ID and Provider if they exist
|
|
58
|
+
const callbacks = props.callbacks || {};
|
|
59
|
+
const translator = createTranslator(i18n);
|
|
60
|
+
const isInvisible = ModeEnum.invisible === config.mode;
|
|
61
|
+
|
|
62
|
+
const store: ProcaptchaStateHandle = createProcaptchaState();
|
|
63
|
+
let loading = false;
|
|
64
|
+
let puzzlePhase: PuzzlePhase = "checkbox";
|
|
65
|
+
let challengeData: GetPuzzleCaptchaResponse | null = null;
|
|
66
|
+
let showRetry = false;
|
|
67
|
+
let lastError: ProcaptchaState["error"] = store.state.error;
|
|
68
|
+
// See procaptcha-pow's widget — same session-invalidation recovery contract
|
|
69
|
+
// with coords preservation across a re-mint.
|
|
70
|
+
let lastCoords: { x: number; y: number } | null = null;
|
|
71
|
+
let sessionInvalidatedFired = false;
|
|
72
|
+
|
|
73
|
+
let honeypot: HoneypotComponent | undefined;
|
|
74
|
+
let checkbox: Component<CheckboxProps> | undefined;
|
|
75
|
+
let puzzle: Component<PuzzleCanvasProps> | undefined;
|
|
76
|
+
|
|
77
|
+
// get the state update mechanism
|
|
78
|
+
const updateState = buildUpdateState(store.state, store.update);
|
|
79
|
+
|
|
80
|
+
const manager = Manager(
|
|
81
|
+
config,
|
|
82
|
+
store.state,
|
|
83
|
+
updateState,
|
|
84
|
+
callbacks,
|
|
85
|
+
frictionlessState,
|
|
86
|
+
() => honeypot?.getValue(),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const root = createElement("div");
|
|
90
|
+
container.appendChild(root);
|
|
91
|
+
|
|
92
|
+
if (frictionlessState?.hp) {
|
|
93
|
+
honeypot = mountHoneypot(root, { encodedQuestion: frictionlessState.hp });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handlePuzzleComplete = async (
|
|
97
|
+
finalX: number,
|
|
98
|
+
finalY: number,
|
|
99
|
+
puzzleEvents: PuzzleEvent[],
|
|
100
|
+
): Promise<void> => {
|
|
101
|
+
puzzlePhase = "submitting";
|
|
102
|
+
scheduler.schedule();
|
|
103
|
+
|
|
104
|
+
let verified = false;
|
|
105
|
+
try {
|
|
106
|
+
verified = await manager.submitSolution(finalX, finalY, puzzleEvents);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
callbacks.onError?.(
|
|
109
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (verified) {
|
|
114
|
+
puzzlePhase = "checkbox";
|
|
115
|
+
challengeData = null;
|
|
116
|
+
showRetry = false;
|
|
117
|
+
loading = false;
|
|
118
|
+
scheduler.schedule();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Failed — show retry message and fetch a new challenge
|
|
123
|
+
showRetry = true;
|
|
124
|
+
puzzlePhase = "dragging";
|
|
125
|
+
scheduler.schedule();
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const newChallenge = await manager.start();
|
|
129
|
+
if (newChallenge) {
|
|
130
|
+
challengeData = newChallenge;
|
|
131
|
+
} else {
|
|
132
|
+
// Couldn't get new challenge, fall back to checkbox
|
|
133
|
+
puzzlePhase = "checkbox";
|
|
134
|
+
challengeData = null;
|
|
135
|
+
showRetry = false;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
puzzlePhase = "checkbox";
|
|
139
|
+
challengeData = null;
|
|
140
|
+
showRetry = false;
|
|
141
|
+
}
|
|
142
|
+
loading = false;
|
|
143
|
+
scheduler.schedule();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Dismissing returns to the checkbox; clicking away is not a wrong answer.
|
|
147
|
+
const handleDismiss = () => {
|
|
148
|
+
puzzlePhase = "checkbox";
|
|
149
|
+
challengeData = null;
|
|
150
|
+
showRetry = false;
|
|
151
|
+
loading = false;
|
|
152
|
+
scheduler.schedule();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const puzzleProps = (
|
|
156
|
+
challenge: GetPuzzleCaptchaResponse,
|
|
157
|
+
): PuzzleCanvasProps => ({
|
|
158
|
+
originX: challenge.originX,
|
|
159
|
+
originY: challenge.originY,
|
|
160
|
+
background: challenge.background,
|
|
161
|
+
piece: challenge.piece,
|
|
162
|
+
pieceSize: challenge.pieceSize,
|
|
163
|
+
onComplete: (finalX: number, finalY: number, events: PuzzleEvent[]) => {
|
|
164
|
+
void handlePuzzleComplete(finalX, finalY, events);
|
|
165
|
+
},
|
|
166
|
+
showRetry,
|
|
167
|
+
submitting: "submitting" === puzzlePhase,
|
|
168
|
+
theme: "light" === config.theme ? lightTheme : darkTheme,
|
|
169
|
+
translator,
|
|
170
|
+
placement: config.placement,
|
|
171
|
+
anchor: props.container,
|
|
172
|
+
onDismiss: handleDismiss,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const runErrorEffect = () => {
|
|
176
|
+
if (store.state.error === lastError) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
lastError = store.state.error;
|
|
180
|
+
if (!store.state.error) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
loading = false;
|
|
184
|
+
puzzlePhase = "checkbox";
|
|
185
|
+
challengeData = null;
|
|
186
|
+
showRetry = false;
|
|
187
|
+
if ("CAPTCHA.NO_SESSION_FOUND" !== store.state.error.key) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (props.onSessionInvalidated && !sessionInvalidatedFired) {
|
|
191
|
+
sessionInvalidatedFired = true;
|
|
192
|
+
props.onSessionInvalidated(lastCoords?.x, lastCoords?.y);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (frictionlessState) {
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
frictionlessState.restart();
|
|
198
|
+
}, 100);
|
|
199
|
+
teardown.add(() => clearTimeout(timer));
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const render = () => {
|
|
204
|
+
runErrorEffect();
|
|
205
|
+
|
|
206
|
+
// Puzzle overlay — shown in both visible and invisible modes once a
|
|
207
|
+
// challenge has been fetched; puzzle is inherently interactive.
|
|
208
|
+
const showOverlay =
|
|
209
|
+
("dragging" === puzzlePhase || "submitting" === puzzlePhase) &&
|
|
210
|
+
null !== challengeData;
|
|
211
|
+
|
|
212
|
+
if (showOverlay && null !== challengeData) {
|
|
213
|
+
if (undefined === puzzle) {
|
|
214
|
+
puzzle = mountPuzzleCanvas(puzzleProps(challengeData));
|
|
215
|
+
} else {
|
|
216
|
+
puzzle.update(puzzleProps(challengeData));
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
puzzle?.destroy();
|
|
220
|
+
puzzle = undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
checkbox?.update(checkboxProps());
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const scheduler = createRenderScheduler(render);
|
|
227
|
+
|
|
228
|
+
const checkboxProps = (): CheckboxProps => ({
|
|
229
|
+
checked: store.state.isHuman,
|
|
230
|
+
theme: "light" === config.theme ? lightTheme : darkTheme,
|
|
231
|
+
labelText: translator.isReady() ? translator.t("WIDGET.I_AM_HUMAN") : "",
|
|
232
|
+
error: store.state.error?.message,
|
|
233
|
+
loadingText: translator.t("WIDGET.CHECKING", {
|
|
234
|
+
defaultValue: "Checking that you are human",
|
|
235
|
+
}),
|
|
236
|
+
loading: loading || "submitting" === puzzlePhase,
|
|
237
|
+
onChange: async (
|
|
238
|
+
event: MouseEvent | KeyboardEvent | TouchEvent,
|
|
239
|
+
): Promise<void> => {
|
|
240
|
+
if (loading) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
loading = true;
|
|
244
|
+
showRetry = false;
|
|
245
|
+
scheduler.schedule();
|
|
246
|
+
|
|
247
|
+
// Capture click coordinates (mirrors the PoW widget) so the puzzle
|
|
248
|
+
// solution salt records the entry-point telemetry.
|
|
249
|
+
let x = 0;
|
|
250
|
+
let y = 0;
|
|
251
|
+
if (!isEventTrusted(event)) {
|
|
252
|
+
// Don't capture coordinates for non-trusted events
|
|
253
|
+
} else if ("touches" in event && event.touches.length > 0) {
|
|
254
|
+
const touch = event.touches[0];
|
|
255
|
+
if (touch) {
|
|
256
|
+
x = touch.clientX;
|
|
257
|
+
y = touch.clientY;
|
|
258
|
+
}
|
|
259
|
+
} else if ("clientX" in event && "clientY" in event) {
|
|
260
|
+
x = event.clientX;
|
|
261
|
+
y = event.clientY;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
lastCoords = { x, y };
|
|
265
|
+
try {
|
|
266
|
+
const challenge = await manager.start(x, y);
|
|
267
|
+
|
|
268
|
+
if (challenge) {
|
|
269
|
+
challengeData = challenge;
|
|
270
|
+
puzzlePhase = "dragging";
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// The manager reports failures through state.error; rethrowing here
|
|
274
|
+
// only produces an unhandled rejection, since nothing awaits this
|
|
275
|
+
// handler.
|
|
276
|
+
callbacks.onError?.(
|
|
277
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
278
|
+
);
|
|
279
|
+
} finally {
|
|
280
|
+
// A rejected start would otherwise leave the spinner up for good,
|
|
281
|
+
// with no way back to the checkbox for the user.
|
|
282
|
+
loading = false;
|
|
283
|
+
scheduler.schedule();
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Checkbox — only in visible mode. Invisible mode is driven by the host
|
|
289
|
+
// page's execute() call (e.g. on form submit).
|
|
290
|
+
if (!isInvisible) {
|
|
291
|
+
checkbox = mountCheckbox(root, checkboxProps());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
teardown.add(scheduler.cancel);
|
|
295
|
+
teardown.add(store.subscribe(scheduler.schedule));
|
|
296
|
+
teardown.add(translator.subscribe(scheduler.schedule));
|
|
297
|
+
|
|
298
|
+
// A bare execute() reaches every invisible widget via document. A targeted
|
|
299
|
+
// execute() is dispatched on this widget's container and works in either
|
|
300
|
+
// mode, which is what lets a bound button drive a visible widget. Either
|
|
301
|
+
// way it fetches a challenge and drives the puzzle UI through the same
|
|
302
|
+
// phase transitions as the visible checkbox flow.
|
|
303
|
+
const handleExecute = () => {
|
|
304
|
+
void (async () => {
|
|
305
|
+
if (loading) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
loading = true;
|
|
309
|
+
showRetry = false;
|
|
310
|
+
scheduler.schedule();
|
|
311
|
+
try {
|
|
312
|
+
const challenge = await manager.start();
|
|
313
|
+
if (challenge) {
|
|
314
|
+
challengeData = challenge;
|
|
315
|
+
puzzlePhase = "dragging";
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
callbacks.onError?.(
|
|
319
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
320
|
+
);
|
|
321
|
+
} finally {
|
|
322
|
+
loading = false;
|
|
323
|
+
scheduler.schedule();
|
|
324
|
+
}
|
|
325
|
+
})();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (props.container) {
|
|
329
|
+
teardown.addEventListener(
|
|
330
|
+
props.container,
|
|
331
|
+
PROCAPTCHA_EXECUTE_EVENT,
|
|
332
|
+
handleExecute,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (isInvisible) {
|
|
336
|
+
teardown.addEventListener(
|
|
337
|
+
document,
|
|
338
|
+
PROCAPTCHA_EXECUTE_EVENT,
|
|
339
|
+
handleExecute,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (config.language) {
|
|
344
|
+
if (i18n) {
|
|
345
|
+
if (i18n.language !== config.language) {
|
|
346
|
+
void i18n.changeLanguage(config.language);
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
// Direct consumers don't go through WidgetFactory, so pass the language
|
|
350
|
+
// into loadI18next — first init boots with the right language (skipping
|
|
351
|
+
// browser detection), and subsequent calls reconcile via changeLanguage
|
|
352
|
+
// inside loadI18next.
|
|
353
|
+
void loadI18next(false, config.language);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (props.autoStart) {
|
|
358
|
+
loading = true;
|
|
359
|
+
showRetry = false;
|
|
360
|
+
const coords = props.startCoords;
|
|
361
|
+
lastCoords = coords ?? null;
|
|
362
|
+
scheduler.schedule();
|
|
363
|
+
manager.start(coords?.x ?? 0, coords?.y ?? 0).then(
|
|
364
|
+
(challenge: GetPuzzleCaptchaResponse | undefined) => {
|
|
365
|
+
if (challenge) {
|
|
366
|
+
challengeData = challenge;
|
|
367
|
+
puzzlePhase = "dragging";
|
|
368
|
+
}
|
|
369
|
+
loading = false;
|
|
370
|
+
scheduler.schedule();
|
|
371
|
+
},
|
|
372
|
+
() => {
|
|
373
|
+
loading = false;
|
|
374
|
+
scheduler.schedule();
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
render();
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
destroy: () => {
|
|
383
|
+
teardown.run();
|
|
384
|
+
puzzle?.destroy();
|
|
385
|
+
checkbox?.destroy();
|
|
386
|
+
honeypot?.destroy();
|
|
387
|
+
root.parentNode?.removeChild(root);
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
export default mountProcaptchaPuzzleWidget;
|