@prosopo/procaptcha-puzzle 2.14.7 → 2.15.0

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.
Files changed (36) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +4 -7
  2. package/.turbo/turbo-build$colon$tsc.log +19 -22
  3. package/.turbo/turbo-build.log +5 -11
  4. package/CHANGELOG.md +30 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/components/procaptchaWidget.cjs +18 -4
  7. package/dist/cjs/components/puzzleCanvas.cjs +25 -4
  8. package/dist/components/procaptchaWidget.d.ts.map +1 -1
  9. package/dist/components/procaptchaWidget.js +18 -4
  10. package/dist/components/procaptchaWidget.js.map +1 -1
  11. package/dist/components/puzzleCanvas.d.ts +2 -1
  12. package/dist/components/puzzleCanvas.d.ts.map +1 -1
  13. package/dist/components/puzzleCanvas.js +26 -5
  14. package/dist/components/puzzleCanvas.js.map +1 -1
  15. package/dist/tests/procaptchaWidget.unit.test.js +62 -0
  16. package/dist/tests/procaptchaWidget.unit.test.js.map +1 -1
  17. package/dist/tests/puzzleCanvas.unit.test.js +33 -0
  18. package/dist/tests/puzzleCanvas.unit.test.js.map +1 -1
  19. package/package.json +72 -71
  20. package/src/components/procaptchaPuzzle.ts +0 -54
  21. package/src/components/procaptchaWidget.ts +0 -429
  22. package/src/components/puzzleCanvas.ts +0 -658
  23. package/src/index.ts +0 -16
  24. package/src/services/Manager.ts +0 -529
  25. package/src/tests/domHarness.ts +0 -122
  26. package/src/tests/invisibleCanvasVisibility.unit.test.ts +0 -181
  27. package/src/tests/manager.unit.test.ts +0 -840
  28. package/src/tests/managerHarness.ts +0 -169
  29. package/src/tests/procaptchaPuzzle.test-d.ts +0 -212
  30. package/src/tests/procaptchaPuzzle.unit.test.ts +0 -138
  31. package/src/tests/procaptchaWidget.unit.test.ts +0 -777
  32. package/src/tests/puzzleCanvas.unit.test.ts +0 -662
  33. package/tsconfig.cjs.json +0 -43
  34. package/tsconfig.json +0 -44
  35. package/tsconfig.tsbuildinfo +0 -1
  36. package/tsconfig.types.json +0 -9
package/package.json CHANGED
@@ -1,72 +1,73 @@
1
1
  {
2
- "name": "@prosopo/procaptcha-puzzle",
3
- "version": "2.14.7",
4
- "author": "PROSOPO LIMITED <info@prosopo.io>",
5
- "license": "Apache-2.0",
6
- "main": "dist/index.js",
7
- "type": "module",
8
- "sideEffects": false,
9
- "engines": {
10
- "node": "^24",
11
- "npm": "^11"
12
- },
13
- "exports": {
14
- ".": {
15
- "types": "./dist/index.d.ts",
16
- "import": "./dist/index.js",
17
- "require": "./dist/cjs/index.cjs"
18
- }
19
- },
20
- "types": "dist/index.d.ts",
21
- "source": "./src/index.ts",
22
- "scripts": {
23
- "clean": "del-cli --verbose dist tsconfig.tsbuildinfo",
24
- "build": "npm run build:cross-env -- --mode ${NODE_ENV:-development}",
25
- "build:cross-env": "vite build --config vite.esm.config.ts",
26
- "build:tsc": "tsc --build --verbose",
27
- "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
28
- "typecheck": "tsc --project tsconfig.types.json",
29
- "test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts",
30
- "test:watch": "NODE_ENV=${NODE_ENV:-test}; npx vitest --config ./vite.test.config.ts",
31
- "test:coverage": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --coverage --config ./vite.test.config.ts"
32
- },
33
- "browserslist": ["> 0.5%, last 2 versions, not dead"],
34
- "dependencies": {
35
- "@polkadot/util": "14.0.3",
36
- "@prosopo/api": "4.3.8",
37
- "@prosopo/common": "3.1.61",
38
- "@prosopo/locale": "3.6.2",
39
- "@prosopo/procaptcha-common": "2.18.2",
40
- "@prosopo/types": "5.12.0",
41
- "@prosopo/util": "3.3.13",
42
- "@prosopo/util-crypto": "13.6.0",
43
- "@prosopo/widget-skeleton": "2.10.1"
44
- },
45
- "devDependencies": {
46
- "@prosopo/config": "3.4.2",
47
- "@types/node": "22.5.5",
48
- "@vitest/coverage-v8": "4.1.11",
49
- "concurrently": "9.0.1",
50
- "del-cli": "6.0.0",
51
- "jsdom": "26.1.0",
52
- "npm-run-all": "4.1.5",
53
- "tslib": "2.7.0",
54
- "tsx": "4.20.3",
55
- "typescript": "5.6.2",
56
- "vite": "8.1.5",
57
- "vitest": "4.1.11"
58
- },
59
- "repository": {
60
- "type": "git",
61
- "url": "git+https://github.com/prosopo/captcha.git",
62
- "directory": "packages/procaptcha-puzzle"
63
- },
64
- "bugs": {
65
- "url": "https://github.com/prosopo/captcha/issues"
66
- },
67
- "homepage": "https://github.com/prosopo/captcha#readme",
68
- "description": "",
69
- "publishConfig": {
70
- "registry": "https://registry.npmjs.org"
71
- }
72
- }
2
+ "name": "@prosopo/procaptcha-puzzle",
3
+ "version": "2.15.0",
4
+ "author": "PROSOPO LIMITED <info@prosopo.io>",
5
+ "license": "Apache-2.0",
6
+ "main": "dist/index.js",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "engines": {
10
+ "node": "^24"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js",
16
+ "require": "./dist/cjs/index.cjs"
17
+ }
18
+ },
19
+ "types": "dist/index.d.ts",
20
+ "source": "./src/index.ts",
21
+ "browserslist": [
22
+ "> 0.5%, last 2 versions, not dead"
23
+ ],
24
+ "dependencies": {
25
+ "@polkadot/util": "14.0.3",
26
+ "@prosopo/api": "4.4.0",
27
+ "@prosopo/common": "3.1.62",
28
+ "@prosopo/locale": "3.7.0",
29
+ "@prosopo/procaptcha-common": "2.19.0",
30
+ "@prosopo/types": "5.13.0",
31
+ "@prosopo/util": "3.3.14",
32
+ "@prosopo/util-crypto": "13.6.0",
33
+ "@prosopo/widget-skeleton": "2.10.1"
34
+ },
35
+ "devDependencies": {
36
+ "@prosopo/config": "3.4.3",
37
+ "@types/node": "22.5.5",
38
+ "@vitest/coverage-v8": "4.1.11",
39
+ "concurrently": "9.0.1",
40
+ "del-cli": "6.0.0",
41
+ "jsdom": "26.1.0",
42
+ "npm-run-all": "4.1.5",
43
+ "tslib": "2.7.0",
44
+ "tsx": "4.20.3",
45
+ "typescript": "5.6.2",
46
+ "vite": "8.1.5",
47
+ "vitest": "4.1.11"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/prosopo/captcha.git",
52
+ "directory": "packages/procaptcha-puzzle"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/prosopo/captcha/issues"
56
+ },
57
+ "homepage": "https://github.com/prosopo/captcha#readme",
58
+ "description": "",
59
+ "publishConfig": {
60
+ "registry": "https://registry.npmjs.org"
61
+ },
62
+ "scripts": {
63
+ "clean": "del-cli --verbose dist tsconfig.tsbuildinfo",
64
+ "build": "pnpm run build:cross-env --mode ${NODE_ENV:-development}",
65
+ "build:cross-env": "vite build --config vite.esm.config.ts",
66
+ "build:tsc": "tsc --build --verbose",
67
+ "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
68
+ "typecheck": "tsc --project tsconfig.types.json",
69
+ "test": "NODE_ENV=${NODE_ENV:-test}; pnpm exec vitest run --config ./vite.test.config.ts",
70
+ "test:watch": "NODE_ENV=${NODE_ENV:-test}; pnpm exec vitest --config ./vite.test.config.ts",
71
+ "test:coverage": "NODE_ENV=${NODE_ENV:-test}; pnpm exec vitest run --coverage --config ./vite.test.config.ts"
72
+ }
73
+ }
@@ -1,54 +0,0 @@
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 type { ProcaptchaProps } from "@prosopo/types";
16
- import type { ProcaptchaPuzzleHandle } from "./procaptchaWidget.js";
17
-
18
- export type { ProcaptchaPuzzleHandle };
19
-
20
- export type ProcaptchaPuzzleMountFn = (
21
- container: HTMLElement,
22
- props: ProcaptchaProps,
23
- ) => ProcaptchaPuzzleHandle;
24
-
25
- /**
26
- * Dynamic import so the puzzle widget lands in its own chunk, replacing the
27
- * `lazy()` + `<Suspense>` pair that used to provide the split.
28
- */
29
- export const loadProcaptchaPuzzle =
30
- async (): Promise<ProcaptchaPuzzleMountFn> =>
31
- (await import("./procaptchaWidget.js")).mountProcaptchaPuzzleWidget;
32
-
33
- export const mountProcaptchaPuzzle = (
34
- container: HTMLElement,
35
- props: ProcaptchaProps,
36
- ): ProcaptchaPuzzleHandle => {
37
- let destroyed = false;
38
- let inner: ProcaptchaPuzzleHandle | undefined;
39
-
40
- void loadProcaptchaPuzzle().then((mount: ProcaptchaPuzzleMountFn) => {
41
- if (destroyed) {
42
- return;
43
- }
44
- inner = mount(container, props);
45
- });
46
-
47
- return {
48
- destroy: () => {
49
- destroyed = true;
50
- inner?.destroy();
51
- inner = undefined;
52
- },
53
- };
54
- };
@@ -1,429 +0,0 @@
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 {
16
- createTranslator,
17
- loadI18next,
18
- localiseErrorMessage,
19
- } from "@prosopo/locale";
20
- import {
21
- type CheckboxProps,
22
- type Component,
23
- type HoneypotComponent,
24
- type ProcaptchaStateHandle,
25
- Teardown,
26
- buildUpdateState,
27
- createElement,
28
- createProcaptchaState,
29
- createRenderScheduler,
30
- isEventTrusted,
31
- mountCheckbox,
32
- mountHoneypot,
33
- } from "@prosopo/procaptcha-common";
34
- import {
35
- type GetPuzzleCaptchaResponse,
36
- ModeEnum,
37
- type ProcaptchaProps,
38
- type ProcaptchaState,
39
- type PuzzleEvent,
40
- } from "@prosopo/types";
41
- import { darkTheme, lightTheme } from "@prosopo/widget-skeleton";
42
- import { Manager } from "../services/Manager.js";
43
- import { type PuzzleCanvasProps, mountPuzzleCanvas } from "./puzzleCanvas.js";
44
-
45
- // Define the same event name as in the bundle for consistency
46
- const PROCAPTCHA_EXECUTE_EVENT = "procaptcha:execute";
47
-
48
- type PuzzlePhase = "checkbox" | "dragging" | "submitting";
49
-
50
- export interface ProcaptchaPuzzleHandle {
51
- destroy(): void;
52
- }
53
-
54
- export const mountProcaptchaPuzzleWidget = (
55
- container: HTMLElement,
56
- props: ProcaptchaProps,
57
- ): ProcaptchaPuzzleHandle => {
58
- const teardown = new Teardown();
59
- const config = props.config;
60
- const i18n = props.i18n;
61
- const frictionlessState = props.frictionlessState; // Set up Session ID and Provider if they exist
62
- const callbacks = props.callbacks || {};
63
- const translator = createTranslator(i18n);
64
- const isInvisible = ModeEnum.invisible === config.mode;
65
-
66
- const store: ProcaptchaStateHandle = createProcaptchaState();
67
- let loading = false;
68
- let puzzlePhase: PuzzlePhase = "checkbox";
69
- let challengeData: GetPuzzleCaptchaResponse | null = null;
70
- // A re-mint after a wrong answer builds a brand new widget, so the miss is
71
- // only still on screen if the wrapper hands it back to us.
72
- let showRetry = true === props.startShowRetry;
73
- let lastError: ProcaptchaState["error"] = store.state.error;
74
- // See procaptcha-pow's widget — same session-invalidation recovery contract
75
- // with coords preservation across a re-mint.
76
- let lastCoords: { x: number; y: number } | null = null;
77
- let sessionInvalidatedFired = false;
78
-
79
- let honeypot: HoneypotComponent | undefined;
80
- let checkbox: Component<CheckboxProps> | undefined;
81
- let puzzle: Component<PuzzleCanvasProps> | undefined;
82
-
83
- // get the state update mechanism
84
- const updateState = buildUpdateState(store.state, store.update);
85
-
86
- const manager = Manager(
87
- config,
88
- store.state,
89
- updateState,
90
- callbacks,
91
- frictionlessState,
92
- () => honeypot?.getValue(),
93
- );
94
-
95
- const root = createElement("div");
96
- container.appendChild(root);
97
-
98
- if (frictionlessState?.hp) {
99
- honeypot = mountHoneypot(root, { encodedQuestion: frictionlessState.hp });
100
- }
101
-
102
- const handlePuzzleComplete = async (
103
- finalX: number,
104
- finalY: number,
105
- puzzleEvents: PuzzleEvent[],
106
- ): Promise<void> => {
107
- puzzlePhase = "submitting";
108
- scheduler.schedule();
109
-
110
- let verified = false;
111
- try {
112
- verified = await manager.submitSolution(finalX, finalY, puzzleEvents);
113
- } catch (error) {
114
- callbacks.onError?.(
115
- error instanceof Error ? error : new Error(String(error)),
116
- );
117
- }
118
-
119
- if (verified) {
120
- puzzlePhase = "checkbox";
121
- challengeData = null;
122
- showRetry = false;
123
- loading = false;
124
- scheduler.schedule();
125
- return;
126
- }
127
-
128
- // Failed — show retry message and fetch a new challenge
129
- showRetry = true;
130
- puzzlePhase = "dragging";
131
- scheduler.schedule();
132
-
133
- // A frictionless session is single-use: the provider consumed it when it
134
- // issued the puzzle the user just got wrong, so asking `manager.start()`
135
- // for a replacement on the same sessionId can only ever come back
136
- // CAPTCHA.NO_SESSION_FOUND — a wasted round trip that surfaces an error
137
- // on the checkbox before the wrapper recovers. Go straight to the
138
- // re-mint instead; the wrapper mints a new session and re-mounts us with
139
- // `autoStart`, so a fresh puzzle appears in place.
140
- if (frictionlessState?.sessionId && props.onReload) {
141
- puzzlePhase = "submitting";
142
- scheduler.schedule();
143
- props.onReload(lastCoords?.x, lastCoords?.y, { showRetry: true });
144
- return;
145
- }
146
-
147
- try {
148
- const newChallenge = await manager.start();
149
- if (newChallenge) {
150
- challengeData = newChallenge;
151
- } else {
152
- // Couldn't get new challenge, fall back to checkbox
153
- puzzlePhase = "checkbox";
154
- challengeData = null;
155
- showRetry = false;
156
- }
157
- } catch {
158
- puzzlePhase = "checkbox";
159
- challengeData = null;
160
- showRetry = false;
161
- }
162
- loading = false;
163
- scheduler.schedule();
164
- };
165
-
166
- // Dismissing returns to the checkbox; clicking away is not a wrong answer.
167
- const handleDismiss = () => {
168
- puzzlePhase = "checkbox";
169
- challengeData = null;
170
- showRetry = false;
171
- loading = false;
172
- scheduler.schedule();
173
- };
174
-
175
- const puzzleProps = (
176
- challenge: GetPuzzleCaptchaResponse,
177
- ): PuzzleCanvasProps => ({
178
- originX: challenge.originX,
179
- originY: challenge.originY,
180
- background: challenge.background,
181
- piece: challenge.piece,
182
- pieceSize: challenge.pieceSize,
183
- onComplete: (finalX: number, finalY: number, events: PuzzleEvent[]) => {
184
- void handlePuzzleComplete(finalX, finalY, events);
185
- },
186
- showRetry,
187
- submitting: "submitting" === puzzlePhase,
188
- theme: "light" === config.theme ? lightTheme : darkTheme,
189
- translator,
190
- placement: config.placement,
191
- anchor: props.container,
192
- onDismiss: handleDismiss,
193
- });
194
-
195
- const runErrorEffect = () => {
196
- if (store.state.error === lastError) {
197
- return;
198
- }
199
- lastError = store.state.error;
200
- if (!store.state.error) {
201
- return;
202
- }
203
- loading = false;
204
- puzzlePhase = "checkbox";
205
- challengeData = null;
206
- showRetry = false;
207
- if ("CAPTCHA.NO_SESSION_FOUND" !== store.state.error.key) {
208
- return;
209
- }
210
- // Suppressed only when something is actually going to re-mint: this is
211
- // an internal recovery signal, not something the user should read, so
212
- // hold the spinner rather than paint a support code that is about to
213
- // stop being true. Clearing it re-enters this effect once, which returns
214
- // at the `!error` guard. With no recovery route the error stands — a
215
- // spinner that never resolves is worse than a message.
216
- const willRecover =
217
- (props.onSessionInvalidated && !sessionInvalidatedFired) ||
218
- undefined !== frictionlessState;
219
- if (willRecover) {
220
- loading = true;
221
- store.update({ error: undefined });
222
- }
223
- if (props.onSessionInvalidated && !sessionInvalidatedFired) {
224
- sessionInvalidatedFired = true;
225
- props.onSessionInvalidated(lastCoords?.x, lastCoords?.y);
226
- return;
227
- }
228
- if (frictionlessState) {
229
- const timer = setTimeout(() => {
230
- frictionlessState.restart();
231
- }, 100);
232
- teardown.add(() => clearTimeout(timer));
233
- }
234
- };
235
-
236
- const render = () => {
237
- runErrorEffect();
238
-
239
- // Puzzle overlay — shown in both visible and invisible modes once a
240
- // challenge has been fetched; puzzle is inherently interactive.
241
- const showOverlay =
242
- ("dragging" === puzzlePhase || "submitting" === puzzlePhase) &&
243
- null !== challengeData;
244
-
245
- if (showOverlay && null !== challengeData) {
246
- if (undefined === puzzle) {
247
- puzzle = mountPuzzleCanvas(puzzleProps(challengeData));
248
- } else {
249
- puzzle.update(puzzleProps(challengeData));
250
- }
251
- } else {
252
- puzzle?.destroy();
253
- puzzle = undefined;
254
- }
255
-
256
- checkbox?.update(checkboxProps());
257
- };
258
-
259
- const scheduler = createRenderScheduler(render);
260
-
261
- const checkboxProps = (): CheckboxProps => ({
262
- checked: store.state.isHuman,
263
- theme: "light" === config.theme ? lightTheme : darkTheme,
264
- labelText: translator.isReady() ? translator.t("WIDGET.I_AM_HUMAN") : "",
265
- error: store.state.error
266
- ? localiseErrorMessage(translator.i18n, store.state.error)
267
- : undefined,
268
- loadingText: translator.t("WIDGET.CHECKING", {
269
- defaultValue: "Checking that you are human",
270
- }),
271
- loading: loading || "submitting" === puzzlePhase,
272
- onChange: async (
273
- event: MouseEvent | KeyboardEvent | TouchEvent,
274
- ): Promise<void> => {
275
- if (loading) {
276
- return;
277
- }
278
- loading = true;
279
- showRetry = false;
280
- scheduler.schedule();
281
-
282
- // Capture click coordinates (mirrors the PoW widget) so the puzzle
283
- // solution salt records the entry-point telemetry.
284
- let x = 0;
285
- let y = 0;
286
- if (!isEventTrusted(event)) {
287
- // Don't capture coordinates for non-trusted events
288
- } else if ("touches" in event && event.touches.length > 0) {
289
- const touch = event.touches[0];
290
- if (touch) {
291
- x = touch.clientX;
292
- y = touch.clientY;
293
- }
294
- } else if ("clientX" in event && "clientY" in event) {
295
- x = event.clientX;
296
- y = event.clientY;
297
- }
298
-
299
- lastCoords = { x, y };
300
- try {
301
- const challenge = await manager.start(x, y);
302
-
303
- if (challenge) {
304
- challengeData = challenge;
305
- puzzlePhase = "dragging";
306
- }
307
- } catch (error) {
308
- // The manager reports failures through state.error; rethrowing here
309
- // only produces an unhandled rejection, since nothing awaits this
310
- // handler.
311
- callbacks.onError?.(
312
- error instanceof Error ? error : new Error(String(error)),
313
- );
314
- } finally {
315
- // A rejected start would otherwise leave the spinner up for good,
316
- // with no way back to the checkbox for the user.
317
- loading = false;
318
- scheduler.schedule();
319
- }
320
- },
321
- });
322
-
323
- // Checkbox — only in visible mode. Invisible mode is driven by the host
324
- // page's execute() call (e.g. on form submit).
325
- if (!isInvisible) {
326
- checkbox = mountCheckbox(root, checkboxProps());
327
- }
328
-
329
- teardown.add(scheduler.cancel);
330
- teardown.add(store.subscribe(scheduler.schedule));
331
- teardown.add(translator.subscribe(scheduler.schedule));
332
-
333
- // A bare execute() reaches every invisible widget via document. A targeted
334
- // execute() is dispatched on this widget's container and works in either
335
- // mode, which is what lets a bound button drive a visible widget. Either
336
- // way it fetches a challenge and drives the puzzle UI through the same
337
- // phase transitions as the visible checkbox flow.
338
- const handleExecute = () => {
339
- void (async () => {
340
- if (loading) {
341
- return;
342
- }
343
- loading = true;
344
- showRetry = false;
345
- scheduler.schedule();
346
- try {
347
- const challenge = await manager.start();
348
- if (challenge) {
349
- challengeData = challenge;
350
- puzzlePhase = "dragging";
351
- }
352
- } catch (error) {
353
- callbacks.onError?.(
354
- error instanceof Error ? error : new Error(String(error)),
355
- );
356
- } finally {
357
- loading = false;
358
- scheduler.schedule();
359
- }
360
- })();
361
- };
362
-
363
- if (props.container) {
364
- teardown.addEventListener(
365
- props.container,
366
- PROCAPTCHA_EXECUTE_EVENT,
367
- handleExecute,
368
- );
369
- }
370
- if (isInvisible) {
371
- teardown.addEventListener(
372
- document,
373
- PROCAPTCHA_EXECUTE_EVENT,
374
- handleExecute,
375
- );
376
- }
377
-
378
- if (config.language) {
379
- if (i18n) {
380
- if (i18n.language !== config.language) {
381
- void i18n.changeLanguage(config.language);
382
- }
383
- } else {
384
- // Direct consumers don't go through WidgetFactory, so pass the language
385
- // into loadI18next — first init boots with the right language (skipping
386
- // browser detection), and subsequent calls reconcile via changeLanguage
387
- // inside loadI18next.
388
- void loadI18next(false, config.language);
389
- }
390
- }
391
-
392
- if (props.autoStart) {
393
- loading = true;
394
- // Deliberately not cleared: an autoStart mount is how a re-mint after a
395
- // wrong answer arrives, and `startShowRetry` says whether it was one.
396
- const coords = props.startCoords;
397
- lastCoords = coords ?? null;
398
- scheduler.schedule();
399
- manager.start(coords?.x ?? 0, coords?.y ?? 0).then(
400
- (challenge: GetPuzzleCaptchaResponse | undefined) => {
401
- if (challenge) {
402
- challengeData = challenge;
403
- puzzlePhase = "dragging";
404
- }
405
- loading = false;
406
- scheduler.schedule();
407
- },
408
- () => {
409
- loading = false;
410
- scheduler.schedule();
411
- },
412
- );
413
- }
414
-
415
- render();
416
-
417
- return {
418
- destroy: () => {
419
- manager.dispose();
420
- teardown.run();
421
- puzzle?.destroy();
422
- checkbox?.destroy();
423
- honeypot?.destroy();
424
- root.parentNode?.removeChild(root);
425
- },
426
- };
427
- };
428
-
429
- export default mountProcaptchaPuzzleWidget;