@prosopo/procaptcha-common 2.9.19 → 2.10.17

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 (107) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +22 -15
  2. package/.turbo/turbo-build$colon$tsc.log +52 -0
  3. package/.turbo/turbo-build.log +26 -16
  4. package/CHANGELOG.md +392 -0
  5. package/dist/callbacks/defaultCallbacks.d.ts +4 -0
  6. package/dist/callbacks/defaultCallbacks.d.ts.map +1 -0
  7. package/dist/callbacks/defaultCallbacks.js.map +1 -0
  8. package/dist/callbacks/defaultEvents.d.ts +14 -0
  9. package/dist/callbacks/defaultEvents.d.ts.map +1 -0
  10. package/dist/callbacks/defaultEvents.js.map +1 -0
  11. package/dist/cjs/elements/form.cjs +8 -2
  12. package/dist/cjs/elements/window.cjs +7 -0
  13. package/dist/cjs/index.cjs +5 -0
  14. package/dist/cjs/reactComponents/Checkbox.cjs +11 -7
  15. package/dist/cjs/reactComponents/Honeypot.cjs +67 -0
  16. package/dist/cjs/reactComponents/TestModeBanner.cjs +47 -0
  17. package/dist/elements/form.d.ts +5 -0
  18. package/dist/elements/form.d.ts.map +1 -0
  19. package/dist/elements/form.js +8 -2
  20. package/dist/elements/form.js.map +1 -0
  21. package/dist/elements/window.d.ts +3 -0
  22. package/dist/elements/window.d.ts.map +1 -0
  23. package/dist/elements/window.js +8 -1
  24. package/dist/elements/window.js.map +1 -0
  25. package/dist/extensionLoader.d.ts +2 -0
  26. package/dist/extensionLoader.d.ts.map +1 -0
  27. package/dist/extensionLoader.js.map +1 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +6 -1
  31. package/dist/index.js.map +1 -0
  32. package/dist/providers.d.ts +4 -0
  33. package/dist/providers.d.ts.map +1 -0
  34. package/dist/providers.js.map +1 -0
  35. package/dist/reactComponents/Checkbox.d.ts +13 -0
  36. package/dist/reactComponents/Checkbox.d.ts.map +1 -0
  37. package/dist/reactComponents/Checkbox.js +11 -7
  38. package/dist/reactComponents/Checkbox.js.map +1 -0
  39. package/dist/reactComponents/Honeypot.d.ts +6 -0
  40. package/dist/reactComponents/Honeypot.d.ts.map +1 -0
  41. package/dist/reactComponents/Honeypot.js +67 -0
  42. package/dist/reactComponents/Honeypot.js.map +1 -0
  43. package/dist/reactComponents/Reload.d.ts +8 -0
  44. package/dist/reactComponents/Reload.d.ts.map +1 -0
  45. package/dist/reactComponents/Reload.js.map +1 -0
  46. package/dist/reactComponents/TestModeBanner.d.ts +7 -0
  47. package/dist/reactComponents/TestModeBanner.d.ts.map +1 -0
  48. package/dist/reactComponents/TestModeBanner.js +47 -0
  49. package/dist/reactComponents/TestModeBanner.js.map +1 -0
  50. package/dist/state/builder.d.ts +9 -0
  51. package/dist/state/builder.d.ts.map +1 -0
  52. package/dist/state/builder.js.map +1 -0
  53. package/dist/tests/defaultCallbacks.test.d.ts +2 -0
  54. package/dist/tests/defaultCallbacks.test.d.ts.map +1 -0
  55. package/dist/tests/defaultCallbacks.test.js +219 -0
  56. package/dist/tests/defaultCallbacks.test.js.map +1 -0
  57. package/dist/tests/defaultEvents.test.d.ts +2 -0
  58. package/dist/tests/defaultEvents.test.d.ts.map +1 -0
  59. package/dist/tests/defaultEvents.test.js +54 -0
  60. package/dist/tests/defaultEvents.test.js.map +1 -0
  61. package/dist/tests/extensionLoader.test.d.ts +2 -0
  62. package/dist/tests/extensionLoader.test.d.ts.map +1 -0
  63. package/dist/tests/extensionLoader.test.js +21 -0
  64. package/dist/tests/extensionLoader.test.js.map +1 -0
  65. package/dist/tests/form.test.d.ts +2 -0
  66. package/dist/tests/form.test.d.ts.map +1 -0
  67. package/dist/tests/form.test.js +98 -0
  68. package/dist/tests/form.test.js.map +1 -0
  69. package/dist/tests/providers.test.d.ts +2 -0
  70. package/dist/tests/providers.test.d.ts.map +1 -0
  71. package/dist/tests/providers.test.js +111 -0
  72. package/dist/tests/providers.test.js.map +1 -0
  73. package/dist/tests/state-builder.test.d.ts +2 -0
  74. package/dist/tests/state-builder.test.d.ts.map +1 -0
  75. package/dist/tests/state-builder.test.js +193 -0
  76. package/dist/tests/state-builder.test.js.map +1 -0
  77. package/dist/tests/window.test.d.ts +2 -0
  78. package/dist/tests/window.test.d.ts.map +1 -0
  79. package/dist/tests/window.test.js +80 -0
  80. package/dist/tests/window.test.js.map +1 -0
  81. package/package.json +13 -9
  82. package/src/callbacks/defaultCallbacks.ts +197 -0
  83. package/src/callbacks/defaultEvents.ts +21 -0
  84. package/src/elements/form.ts +41 -0
  85. package/src/elements/window.ts +39 -0
  86. package/src/extensionLoader.ts +17 -0
  87. package/src/index.ts +24 -0
  88. package/src/providers.ts +49 -0
  89. package/src/reactComponents/Checkbox.tsx +203 -0
  90. package/src/reactComponents/Honeypot.tsx +133 -0
  91. package/src/reactComponents/Reload.tsx +99 -0
  92. package/src/reactComponents/TestModeBanner.tsx +75 -0
  93. package/src/state/builder.ts +137 -0
  94. package/src/tests/defaultCallbacks.test.ts +372 -0
  95. package/src/tests/defaultEvents.test.ts +80 -0
  96. package/src/tests/extensionLoader.test.ts +41 -0
  97. package/src/tests/form.test.ts +154 -0
  98. package/src/tests/providers.test.ts +175 -0
  99. package/src/tests/state-builder.test.ts +264 -0
  100. package/src/tests/window.test.ts +137 -0
  101. package/tsconfig.cjs.json +32 -0
  102. package/tsconfig.json +33 -0
  103. package/tsconfig.tsbuildinfo +1 -0
  104. package/tsconfig.types.json +9 -0
  105. package/vite.cjs.config.ts +1 -1
  106. package/vite.esm.config.ts +1 -1
  107. package/vite.test.config.ts +1 -1
@@ -0,0 +1,17 @@
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
+ export const ExtensionLoader = async (web2: boolean) =>
15
+ web2
16
+ ? (await import("@prosopo/account/extension/ExtensionWeb2")).default
17
+ : (await import("@prosopo/account/extension/ExtensionWeb3")).default;
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
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
+ export * from "./providers.js";
16
+ export * from "./state/builder.js";
17
+ export * from "./callbacks/defaultCallbacks.js";
18
+ export * from "./callbacks/defaultEvents.js";
19
+ export * from "./extensionLoader.js";
20
+ export * from "./elements/window.js";
21
+ export * from "./reactComponents/Reload.js";
22
+ export * from "./reactComponents/Checkbox.js";
23
+ export * from "./reactComponents/Honeypot.js";
24
+ export * from "./reactComponents/TestModeBanner.js";
@@ -0,0 +1,49 @@
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 { getRandomActiveProvider } from "@prosopo/load-balancer";
16
+ import type { EnvironmentTypes } from "@prosopo/types";
17
+
18
+ export const getProcaptchaRandomActiveProvider = async (
19
+ defaultEnvironment: EnvironmentTypes,
20
+ ) => {
21
+ const randomNumberU8a = window.crypto.getRandomValues(new Uint8Array(10));
22
+ const randomNumber = randomNumberU8a.reduce((a, b) => a + b, 0);
23
+ return await getRandomActiveProvider(defaultEnvironment, randomNumber);
24
+ };
25
+
26
+ export const providerRetry = async (
27
+ currentFn: () => Promise<void>,
28
+ retryFn: () => Promise<void>,
29
+ stateReset: () => void,
30
+ attemptCount: number,
31
+ retryMax: number,
32
+ ) => {
33
+ try {
34
+ await currentFn();
35
+ } catch (err) {
36
+ if (attemptCount >= retryMax) {
37
+ console.error(err);
38
+ console.error(
39
+ `Max retries (${attemptCount} of ${retryMax}) reached, aborting`,
40
+ );
41
+ return stateReset();
42
+ }
43
+ console.error(err);
44
+ // hit an error, disallow user's claim to be human
45
+ stateReset();
46
+ // trigger a retry to attempt a new provider until it passes
47
+ await retryFn();
48
+ }
49
+ };
@@ -0,0 +1,203 @@
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 { css } from "@emotion/react";
16
+ import styled from "@emotion/styled";
17
+ import {
18
+ type Theme,
19
+ WIDGET_CHECKBOX_SPINNER_CSS_CLASS,
20
+ } from "@prosopo/widget-skeleton";
21
+ import {
22
+ type ButtonHTMLAttributes,
23
+ type CSSProperties,
24
+ type FC,
25
+ useMemo,
26
+ useState,
27
+ } from "react";
28
+
29
+ interface CheckboxProps extends ButtonHTMLAttributes<HTMLButtonElement> {
30
+ theme: Theme;
31
+ checked: boolean;
32
+ // biome-ignore lint/suspicious/noExplicitAny: don't know what it will be
33
+ onChange: (event: any) => Promise<void>;
34
+ labelText: string;
35
+ error?: string;
36
+ loading: boolean;
37
+ }
38
+
39
+ const checkboxBefore = css`{
40
+ &:before {
41
+ content: '""';
42
+ position: absolute;
43
+ height: 100%;
44
+ width: 100%;
45
+ }
46
+ }`;
47
+
48
+ const baseStyle: CSSProperties = {
49
+ width: "28px",
50
+ height: "28px",
51
+ minWidth: "14px",
52
+ minHeight: "14px",
53
+ top: "auto",
54
+ left: "auto",
55
+ opacity: "1",
56
+ borderRadius: "12.5%",
57
+ appearance: "none",
58
+ cursor: "pointer",
59
+ margin: "0",
60
+ borderStyle: "solid",
61
+ borderWidth: "1px",
62
+ };
63
+
64
+ const ID_LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
65
+
66
+ const FAQ_LINK = process.env.PROSOPO_DOCS_URL
67
+ ? `${new URL(`${process.env.PROSOPO_DOCS_URL}/en/basics/faq/`).href}/`
68
+ : "https://docs.prosopo.io/en/basics/faq/";
69
+
70
+ const generateRandomId = () => {
71
+ return Array.from(
72
+ { length: 8 },
73
+ () => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)],
74
+ ).join("");
75
+ };
76
+
77
+ interface ResponsiveLabelProps {
78
+ htmFor?: string;
79
+ }
80
+
81
+ export const Checkbox: FC<CheckboxProps> = ({
82
+ theme,
83
+ onChange,
84
+ checked,
85
+ labelText,
86
+ error,
87
+ loading,
88
+ }: CheckboxProps) => {
89
+ const checkboxStyleBase: CSSProperties = {
90
+ ...baseStyle,
91
+ border: `1px solid ${theme.palette.background.contrastText}`,
92
+ };
93
+ const [hover, setHover] = useState(false);
94
+
95
+ const ResponsiveLabel = styled.label<ResponsiveLabelProps>`
96
+ color: ${theme.palette.background.contrastText};
97
+ position: relative;
98
+ display: flex !important;
99
+ cursor: pointer;
100
+ user-select: none;
101
+ font-weight: normal;
102
+ font-family: ${theme.font.fontFamily};
103
+ @container prosopo-widget (max-width: 169px) {
104
+ display: none;
105
+ }
106
+ @container prosopo-widget (min-width: 170px) {
107
+ font-size: 10px;
108
+ }
109
+ @container prosopo-widget (min-width: 220px) {
110
+ font-size: 12px;
111
+ }
112
+ @container prosopo-widget (min-width: 250px) {
113
+ font-size: 14px;
114
+ }
115
+ @container prosopo-widget (min-width: 270px) {
116
+ font-size: 16px;
117
+ }
118
+ `;
119
+
120
+ // biome-ignore lint/correctness/useExhaustiveDependencies: TODO fix
121
+ const checkboxStyle: CSSProperties = useMemo(() => {
122
+ return {
123
+ ...checkboxStyleBase,
124
+ borderColor: hover
125
+ ? theme.palette.background.contrastText
126
+ : theme.palette.border,
127
+ appearance: checked ? "auto" : "none",
128
+ flex: 1,
129
+ margin: "15px",
130
+ minWidth: "28px",
131
+ minHeight: "28px",
132
+ };
133
+ }, [hover, theme, checked]);
134
+ const id = generateRandomId();
135
+
136
+ return (
137
+ <span
138
+ style={{
139
+ display: "inline-flex",
140
+ alignItems: "center",
141
+ minHeight: "58px",
142
+ }}
143
+ >
144
+ {loading ? (
145
+ <div
146
+ className={WIDGET_CHECKBOX_SPINNER_CSS_CLASS}
147
+ aria-label="Loading spinner"
148
+ />
149
+ ) : (
150
+ <input
151
+ name={id}
152
+ id={id}
153
+ onMouseEnter={() => setHover(true)}
154
+ onMouseLeave={() => setHover(false)}
155
+ css={checkboxBefore}
156
+ type={"checkbox"}
157
+ aria-live={"assertive"}
158
+ aria-label={labelText}
159
+ onKeyDown={(e) => {
160
+ if (!e.isTrusted) {
161
+ return;
162
+ }
163
+ if (e.key === "Enter") {
164
+ e.preventDefault();
165
+ e.stopPropagation();
166
+ setHover(false);
167
+ onChange(e);
168
+ }
169
+ }}
170
+ onChange={(e) => {
171
+ if (!e.isTrusted) {
172
+ return;
173
+ }
174
+ e.preventDefault();
175
+ e.stopPropagation();
176
+ setHover(false);
177
+ onChange(e);
178
+ }}
179
+ checked={checked}
180
+ style={checkboxStyle}
181
+ disabled={error !== undefined}
182
+ className={loading ? "prosopo-checkbox__loading-spinner" : ""}
183
+ data-cy={"captcha-checkbox"}
184
+ />
185
+ )}
186
+ {error ? (
187
+ <ResponsiveLabel htmFor={id}>
188
+ <a
189
+ css={{
190
+ color: theme.palette.error.main,
191
+ }}
192
+ href={FAQ_LINK}
193
+ >
194
+ {error}
195
+ </a>
196
+ </ResponsiveLabel>
197
+ ) : (
198
+ <ResponsiveLabel htmFor={id}>{labelText}</ResponsiveLabel>
199
+ )}
200
+ </span>
201
+ );
202
+ };
203
+ export default Checkbox;
@@ -0,0 +1,133 @@
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
+ type CSSProperties,
17
+ forwardRef,
18
+ useEffect,
19
+ useId,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from "react";
24
+ import { createPortal } from "react-dom";
25
+
26
+ interface HoneypotProps {
27
+ encodedQuestion: string;
28
+ }
29
+
30
+ const offscreenStyle: CSSProperties = {
31
+ position: "absolute",
32
+ left: "-9999px",
33
+ top: "-9999px",
34
+ width: "1px",
35
+ height: "1px",
36
+ overflow: "hidden",
37
+ opacity: 0,
38
+ };
39
+
40
+ // Server wraps morse/semaphore in base64 (utf-8) for the wire. Strip the
41
+ // base64 layer here so the rendered label is the raw morse/semaphore an agent
42
+ // can recognise and engage with.
43
+ const decodeBase64Utf8 = (b64: string): string => {
44
+ const binary = atob(b64);
45
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
46
+ return new TextDecoder().decode(bytes);
47
+ };
48
+
49
+ // Locate the dapp's enclosing <form> by stepping out of the widget's shadow
50
+ // root into light DOM, then walking up via closest(). Returns null when the
51
+ // widget isn't embedded inside a form.
52
+ const findAncestorForm = (anchor: Element): HTMLFormElement | null => {
53
+ const root = anchor.getRootNode();
54
+ const lightDomEntry = root instanceof ShadowRoot ? root.host : anchor;
55
+ return lightDomEntry instanceof Element
56
+ ? lightDomEntry.closest("form")
57
+ : null;
58
+ };
59
+
60
+ // Honeypot must live in light DOM, not in the widget's shadow root: if it
61
+ // rendered there a bot would have to traverse `.shadowRoot` to reach it, and
62
+ // @prosopo/catcher patches that getter to detect (and restart on) automated
63
+ // access — wiping the value the bot just wrote before it can submit.
64
+ //
65
+ // Within light DOM we prefer the enclosing <form> so bots scraping
66
+ // `form.querySelectorAll('input')` discover the bait naturally; document.body
67
+ // is the fallback for widgets mounted outside any form.
68
+ //
69
+ // The decoded question is rendered as the input's <label>, not as its value.
70
+ // Naive form-fillers leave the empty input alone — no signal, no false
71
+ // positives. Agents that read the DOM as a prompt may decode the label and
72
+ // write an answer into the empty field; that response rides up as
73
+ // clientMetaData.hp.
74
+ export const Honeypot = forwardRef<HTMLInputElement, HoneypotProps>(
75
+ ({ encodedQuestion }, ref) => {
76
+ const id = useId();
77
+ // Opaque non-existent form id. Setting `form="..."` on an input with a
78
+ // value that doesn't match any form's id disassociates the input from
79
+ // every form: the browser excludes it from the parent form's submission
80
+ // set and from `form.elements`, while leaving it discoverable via
81
+ // `form.querySelectorAll('input')`. Built from useId so it's unique per
82
+ // Honeypot instance and guaranteed not to collide with the input's own id.
83
+ const detachedFormId = `${id}-d`;
84
+ const question = useMemo(
85
+ () => decodeBase64Utf8(encodedQuestion),
86
+ [encodedQuestion],
87
+ );
88
+ const anchorRef = useRef<HTMLSpanElement>(null);
89
+ const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);
90
+
91
+ useEffect(() => {
92
+ const anchor = anchorRef.current;
93
+ if (!anchor) return;
94
+ setPortalTarget(findAncestorForm(anchor) ?? document.body);
95
+ }, []);
96
+
97
+ if (typeof document === "undefined") return null;
98
+
99
+ // Transient anchor while we resolve the portal target. Sits in the React
100
+ // tree (inside the shadow root) just long enough for the effect above to
101
+ // walk out to light DOM and find the form.
102
+ if (!portalTarget) {
103
+ return (
104
+ <span ref={anchorRef} aria-hidden="true" style={{ display: "none" }} />
105
+ );
106
+ }
107
+
108
+ // Input is nested inside the label (implicit association) instead of
109
+ // htmlFor-linked — biome's noLabelWithoutControl rule only recognises
110
+ // the descendant form of association at static-analysis time.
111
+ return createPortal(
112
+ <div aria-hidden="true" style={offscreenStyle}>
113
+ <label>
114
+ {question}
115
+ <input
116
+ ref={ref}
117
+ id={id}
118
+ form={detachedFormId}
119
+ type="text"
120
+ name="email_confirm"
121
+ defaultValue=""
122
+ tabIndex={-1}
123
+ autoComplete="off"
124
+ aria-hidden="true"
125
+ />
126
+ </label>
127
+ </div>,
128
+ portalTarget,
129
+ );
130
+ },
131
+ );
132
+
133
+ Honeypot.displayName = "Honeypot";
@@ -0,0 +1,99 @@
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 { darkTheme, lightTheme } from "@prosopo/widget-skeleton";
16
+ import { type ButtonHTMLAttributes, type FC, useMemo, useState } from "react";
17
+
18
+ interface ReloadButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
19
+ themeColor: "light" | "dark";
20
+ onReload: () => void;
21
+ }
22
+
23
+ const buttonStyleBase = {
24
+ border: "none",
25
+ paddingTop: "6px",
26
+ paddingBottom: "6px",
27
+ cursor: "pointer",
28
+ height: "39px",
29
+ width: "39px",
30
+ borderRadius: "50%",
31
+ display: "flex",
32
+ };
33
+
34
+ export const ReloadButton: FC<ReloadButtonProps> = ({
35
+ themeColor,
36
+ onReload,
37
+ }: ReloadButtonProps) => {
38
+ const theme = useMemo(
39
+ () => (themeColor === "light" ? lightTheme : darkTheme),
40
+ [themeColor],
41
+ );
42
+ const [hover, setHover] = useState(false);
43
+ const buttonStyle = useMemo(() => {
44
+ const baseStyle = {
45
+ ...buttonStyleBase,
46
+ backgroundColor: theme.palette.background.default,
47
+ color: hover
48
+ ? theme.palette.primary.contrastText
49
+ : theme.palette.background.contrastText,
50
+ border: `1px solid ${theme.palette.grey[500]}`,
51
+ borderRadius: "50%",
52
+ transition: "background-color 0.3s",
53
+ boxShadow: `0px 1px 3px 0px ${theme.palette.grey[500]}`,
54
+ justifyContent: "center",
55
+ alignItems: "center",
56
+ margin: "0 auto",
57
+ };
58
+ return {
59
+ ...baseStyle,
60
+ backgroundColor: hover
61
+ ? theme.palette.grey[700]
62
+ : theme.palette.background.default,
63
+ };
64
+ }, [hover, theme]);
65
+ return (
66
+ <button
67
+ className="reload-button"
68
+ aria-label="Reload"
69
+ type="button"
70
+ style={buttonStyle}
71
+ onMouseEnter={() => setHover(true)}
72
+ onMouseLeave={() => setHover(false)}
73
+ onClick={(e) => {
74
+ e.preventDefault();
75
+ onReload();
76
+ }}
77
+ >
78
+ <svg
79
+ width="16px"
80
+ height="16px"
81
+ viewBox="0 0 16 16"
82
+ version="1.1"
83
+ xmlns="http://www.w3.org/2000/svg"
84
+ xmlnsXlink="http://www.w3.org/1999/xlink"
85
+ style={{ display: "flex" }}
86
+ >
87
+ <title>reload</title>
88
+ <path
89
+ shapeRendering="optimizeQuality"
90
+ fill={
91
+ hover ? theme.palette.primary.contrastText : theme.palette.grey[700]
92
+ }
93
+ transform={"scale(0.0416)"}
94
+ d="M234.666667,149.333333 L234.666667,106.666667 L314.564847,106.664112 C287.579138,67.9778918 242.745446,42.6666667 192,42.6666667 C109.525477,42.6666667 42.6666667,109.525477 42.6666667,192 C42.6666667,274.474523 109.525477,341.333333 192,341.333333 C268.201293,341.333333 331.072074,284.258623 340.195444,210.526102 L382.537159,215.817985 C370.807686,310.617565 289.973536,384 192,384 C85.961328,384 1.42108547e-14,298.038672 1.42108547e-14,192 C1.42108547e-14,85.961328 85.961328,1.42108547e-14 192,1.42108547e-14 C252.316171,1.42108547e-14 306.136355,27.8126321 341.335366,71.3127128 L341.333333,1.42108547e-14 L384,1.42108547e-14 L384,149.333333 L234.666667,149.333333 Z"
95
+ />
96
+ </svg>
97
+ </button>
98
+ );
99
+ };
@@ -0,0 +1,75 @@
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
+ /** @jsxImportSource @emotion/react */
16
+
17
+ import { TestSiteKeyMode, getTestSiteKeyMode } from "@prosopo/types";
18
+ import { type FC, useEffect } from "react";
19
+
20
+ interface TestModeBannerProps {
21
+ siteKey: string;
22
+ }
23
+
24
+ // Renders a prominent warning when the widget is configured with one of the
25
+ // reserved CI test site keys (always-pass / always-fail). Renders nothing for a
26
+ // normal site key. This is the user-facing safeguard that stops a test key from
27
+ // being shipped to production unnoticed.
28
+ export const TestModeBanner: FC<TestModeBannerProps> = ({
29
+ siteKey,
30
+ }: TestModeBannerProps) => {
31
+ const mode: TestSiteKeyMode | null = getTestSiteKeyMode(siteKey);
32
+
33
+ useEffect(() => {
34
+ if (mode !== null) {
35
+ console.warn(
36
+ `[Procaptcha] WARNING: site key "${siteKey}" is a TEST key that ALWAYS ${
37
+ mode === TestSiteKeyMode.Pass ? "PASSES" : "FAILS"
38
+ }. Never use it in production.`,
39
+ );
40
+ }
41
+ }, [mode, siteKey]);
42
+
43
+ if (mode === null) {
44
+ return null;
45
+ }
46
+
47
+ const action =
48
+ mode === TestSiteKeyMode.Pass ? "ALWAYS PASSES" : "ALWAYS FAILS";
49
+
50
+ return (
51
+ // biome-ignore lint/a11y/useSemanticElements: the "alert" role has no native HTML element equivalent
52
+ <div
53
+ role="alert"
54
+ data-cy="test-mode-banner"
55
+ css={{
56
+ width: "100%",
57
+ boxSizing: "border-box",
58
+ padding: "6px 10px",
59
+ backgroundColor: "#fff3cd",
60
+ color: "#664d03",
61
+ border: "1px solid #ffe69c",
62
+ borderRadius: "4px",
63
+ fontSize: "11px",
64
+ lineHeight: "1.3",
65
+ fontFamily:
66
+ "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
67
+ textAlign: "center",
68
+ }}
69
+ >
70
+ {`⚠ Test mode: this site key ${action}. Do not use in production.`}
71
+ </div>
72
+ );
73
+ };
74
+
75
+ export default TestModeBanner;