Skip to content

Commit cd4c650

Browse files
chore(js/frontend): migrate faucet verification to Cloudflare Turnstile (#815)
1 parent 1a3d3ea commit cd4c650

File tree

7 files changed

+88
-50
lines changed

7 files changed

+88
-50
lines changed

.github/workflows/deploy-frontend.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
VITE_ERC20_MANAGER_CONTRACT_ADDRESSES=${{ secrets.VITE_ERC20_MANAGER_CONTRACT_ADDRESSES }}
9595
VITE_TOKEN_PRICE_API_URL=${{ secrets.VITE_TOKEN_PRICE_API_URL }}
9696
VITE_FAUCET_API_URL=${{ secrets.VITE_FAUCET_API_URL }}
97-
VITE_HCAPTCHA_SITEKEY=${{ secrets.VITE_HCAPTCHA_SITEKEY }}
97+
VITE_TURNSTILE_SITEKEY=${{ secrets.VITE_TURNSTILE_SITEKEY }}
9898
VITE_VARA_ARCHIVE_NODE_ADDRESSES=${{ secrets.VITE_VARA_ARCHIVE_NODE_ADDRESSES }}
9999
VITE_ETH_BEACON_NODE_ADDRESSES=${{ secrets.VITE_ETH_BEACON_NODE_ADDRESSES }}
100100
VITE_ETH_MESSAGE_QUEUE_CONTRACT_ADDRESSES=${{ secrets.VITE_ETH_MESSAGE_QUEUE_CONTRACT_ADDRESSES }}

js/frontend/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ ARG VITE_VARA_NODE_ADDRESSES \
2525
VITE_ERC20_MANAGER_CONTRACT_ADDRESSES \
2626
VITE_TOKEN_PRICE_API_URL \
2727
VITE_FAUCET_API_URL \
28-
VITE_HCAPTCHA_SITEKEY \
28+
VITE_TURNSTILE_SITEKEY \
2929
VITE_VARA_ARCHIVE_NODE_ADDRESSES \
3030
VITE_ETH_BEACON_NODE_ADDRESSES \
3131
VITE_ETH_MESSAGE_QUEUE_CONTRACT_ADDRESSES \
@@ -42,7 +42,7 @@ ENV VITE_VARA_NODE_ADDRESSES=${VITE_VARA_NODE_ADDRESSES} \
4242
VITE_ERC20_MANAGER_CONTRACT_ADDRESSES=${VITE_ERC20_MANAGER_CONTRACT_ADDRESSES} \
4343
VITE_TOKEN_PRICE_API_URL=${VITE_TOKEN_PRICE_API_URL} \
4444
VITE_FAUCET_API_URL=${VITE_FAUCET_API_URL} \
45-
VITE_HCAPTCHA_SITEKEY=${VITE_HCAPTCHA_SITEKEY} \
45+
VITE_TURNSTILE_SITEKEY=${VITE_TURNSTILE_SITEKEY} \
4646
VITE_VARA_ARCHIVE_NODE_ADDRESSES=${VITE_VARA_ARCHIVE_NODE_ADDRESSES} \
4747
VITE_ETH_BEACON_NODE_ADDRESSES=${VITE_ETH_BEACON_NODE_ADDRESSES} \
4848
VITE_ETH_MESSAGE_QUEUE_CONTRACT_ADDRESSES=${VITE_ETH_MESSAGE_QUEUE_CONTRACT_ADDRESSES} \

js/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"@gear-js/ui": "0.7.0",
1818
"@gear-js/vara-ui": "0.3.1",
1919
"@gear-js/wallet-connect": "0.4.3",
20-
"@hcaptcha/react-hcaptcha": "1.17.1",
2120
"@hookform/resolvers": "5.2.2",
21+
"@marsidev/react-turnstile": "1.4.2",
2222
"@polkadot/api": "16.4.6",
2323
"@polkadot/react-identicon": "3.16.4",
2424
"@reown/appkit": "1.8.15",

js/frontend/src/features/faucet/components/get-balance-button/get-balance-button.module.scss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,25 @@
33
--padding: 4px 8px;
44
--spinner-size: 14px;
55
}
6+
7+
.overlay {
8+
display: none;
9+
10+
width: 100%;
11+
height: 100%;
12+
padding: 0 32px;
13+
14+
position: fixed;
15+
top: 0;
16+
left: 0;
17+
z-index: 10;
18+
19+
background-color: #0003;
20+
backdrop-filter: blur(20px);
21+
22+
&.active {
23+
display: flex;
24+
align-items: center;
25+
justify-content: center;
26+
}
27+
}

js/frontend/src/features/faucet/components/get-balance-button/get-balance-button.tsx

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { HexString } from '@gear-js/api';
22
import { useAccount, useAlert, useApi } from '@gear-js/react-hooks';
33
import { Button } from '@gear-js/vara-ui';
4-
import HCaptcha from '@hcaptcha/react-hcaptcha';
4+
import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile';
55
import { useMutation } from '@tanstack/react-query';
6-
import { useRef } from 'react';
6+
import { useRef, useState } from 'react';
77

88
import { LinkButton } from '@/components';
99
import { useNetworkType } from '@/context/network-type';
10-
import { useEthAccount } from '@/hooks';
10+
import { useEthAccount, useModal } from '@/hooks';
11+
import { cx } from '@/utils';
1112

1213
import { GetBalanceParameters, getEthTokenBalance, getVaraAccountBalance } from '../../api';
1314
import GiftSVG from '../../assets/gift.svg?react';
14-
import { HCAPTCHA_SITEKEY } from '../../consts';
15+
import { TURNSTILE_SITEKEY } from '../../consts';
1516

1617
import styles from './get-balance-button.module.scss';
1718

@@ -29,38 +30,63 @@ const BUTTON_PROPS = {
2930

3031
function ButtonComponent<T>({ getBalance, onSuccess, ...parameters }: Props<T>) {
3132
const alert = useAlert();
32-
const hCaptchaRef = useRef<HCaptcha>(null);
33+
const turnstileRef = useRef<TurnstileInstance>(null);
34+
35+
const [isVerifying, setIsVerifying] = useState(false);
36+
const [isVerificationVisible, openVerification, closeVerification] = useModal();
3337

3438
const { mutateAsync, isPending } = useMutation({
35-
mutationFn: async () => {
36-
if (!hCaptchaRef.current) throw new Error('HCaptcha ref is null');
39+
mutationFn: (token: string) => getBalance({ token, ...(parameters as T) }),
40+
});
3741

38-
const token = (await hCaptchaRef.current.execute({ async: true })).response;
39-
const payload = parameters as T;
42+
const handleClick = () => {
43+
setIsVerifying(true);
4044

41-
return getBalance({ token, ...payload });
42-
},
43-
});
45+
turnstileRef.current?.reset();
46+
turnstileRef.current?.execute();
47+
};
48+
49+
const settleVerification = () => {
50+
closeVerification();
51+
setIsVerifying(false);
52+
};
4453

45-
const handleClick = () =>
46-
mutateAsync()
54+
const handleVerificationSuccess = (token: string) => {
55+
settleVerification();
56+
57+
mutateAsync(token)
4758
.then(() => {
4859
onSuccess?.();
60+
4961
alert.success(
5062
'Your request for test tokens has been received and is being processed. The tokens will appear in your balance shortly.',
5163
);
5264
})
53-
.catch((error: string | Error) => {
54-
if (error === 'challenge-closed') return;
65+
.catch((error) => alert.error(error instanceof Error ? error.message : String(error)));
66+
};
67+
68+
const handleVerificationError = (code: string) => {
69+
settleVerification();
5570

56-
alert.error(error instanceof Error ? error.message : error);
57-
});
71+
alert.error(`Error verifying that you are a human, code: ${code}. Please try again.`);
72+
};
5873

5974
return (
60-
<div>
61-
<Button onClick={handleClick} isLoading={isPending} {...BUTTON_PROPS} />
62-
<HCaptcha ref={hCaptchaRef} theme="dark" size="invisible" sitekey={HCAPTCHA_SITEKEY} />
63-
</div>
75+
<>
76+
<Button onClick={handleClick} isLoading={isPending || isVerifying} {...BUTTON_PROPS} />
77+
78+
<div className={cx(styles.overlay, isVerificationVisible && styles.active)}>
79+
<Turnstile
80+
options={{ execution: 'execute', appearance: 'interaction-only' }}
81+
siteKey={TURNSTILE_SITEKEY}
82+
ref={turnstileRef}
83+
onBeforeInteractive={openVerification}
84+
onAfterInteractive={settleVerification}
85+
onError={handleVerificationError}
86+
onSuccess={handleVerificationSuccess}
87+
/>
88+
</div>
89+
</>
6490
);
6591
}
6692

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
const HCAPTCHA_SITEKEY = import.meta.env.VITE_HCAPTCHA_SITEKEY as string;
1+
const TURNSTILE_SITEKEY = import.meta.env.VITE_TURNSTILE_SITEKEY as string;
22

3-
export { HCAPTCHA_SITEKEY };
3+
export { TURNSTILE_SITEKEY };

yarn.lock

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ __metadata:
208208
languageName: node
209209
linkType: hard
210210

211-
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.17.9, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
211+
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.10, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
212212
version: 7.28.3
213213
resolution: "@babel/runtime@npm:7.28.3"
214214
checksum: 10c0/b360f82c2c5114f2a062d4d143d7b4ec690094764853937110585a9497977aed66c102166d0e404766c274e02a50ffb8f6d77fef7251ecf3f607f0e03e6397bc
@@ -2017,26 +2017,6 @@ __metadata:
20172017
languageName: node
20182018
linkType: hard
20192019

2020-
"@hcaptcha/loader@npm:^2.3.0":
2021-
version: 2.3.0
2022-
resolution: "@hcaptcha/loader@npm:2.3.0"
2023-
checksum: 10c0/2273c0de35968bad169ba1102085930da52e6a5d3b74bfdc5c00a8959066f9671ddf2f1d25dddbc7544f290268439559cb49b42009d88c5a7e8481dcb9c67981
2024-
languageName: node
2025-
linkType: hard
2026-
2027-
"@hcaptcha/react-hcaptcha@npm:1.17.1":
2028-
version: 1.17.1
2029-
resolution: "@hcaptcha/react-hcaptcha@npm:1.17.1"
2030-
dependencies:
2031-
"@babel/runtime": "npm:^7.17.9"
2032-
"@hcaptcha/loader": "npm:^2.3.0"
2033-
peerDependencies:
2034-
react: ">= 16.3.0"
2035-
react-dom: ">= 16.3.0"
2036-
checksum: 10c0/f807130fa9068ef096caa6cb97ab8d922c3f78bae6a22fc9a79b01caf47189555b36bd72585119783b9a271d369854ce572cea0df9c29c75ac349f9ba1abf05a
2037-
languageName: node
2038-
linkType: hard
2039-
20402020
"@hookform/resolvers@npm:5.2.2":
20412021
version: 5.2.2
20422022
resolution: "@hookform/resolvers@npm:5.2.2"
@@ -2549,6 +2529,16 @@ __metadata:
25492529
languageName: node
25502530
linkType: hard
25512531

2532+
"@marsidev/react-turnstile@npm:1.4.2":
2533+
version: 1.4.2
2534+
resolution: "@marsidev/react-turnstile@npm:1.4.2"
2535+
peerDependencies:
2536+
react: ^17.0.2 || ^18.0.0 || ^19.0
2537+
react-dom: ^17.0.2 || ^18.0.0 || ^19.0
2538+
checksum: 10c0/3cb72435076f65eb1bf965ad2aaa15900df032bf32609246591a8810f633d508f98561b9a2376c9b3c15f36de639b885df6bf90d3001f3ffb086f29e3396945a
2539+
languageName: node
2540+
linkType: hard
2541+
25522542
"@metamask/eth-json-rpc-provider@npm:^1.0.0":
25532543
version: 1.0.1
25542544
resolution: "@metamask/eth-json-rpc-provider@npm:1.0.1"
@@ -13006,8 +12996,8 @@ __metadata:
1300612996
"@graphql-codegen/cli": "npm:6.1.0"
1300712997
"@graphql-codegen/client-preset": "npm:5.2.2"
1300812998
"@graphql-typed-document-node/core": "npm:3.2.0"
13009-
"@hcaptcha/react-hcaptcha": "npm:1.17.1"
1301012999
"@hookform/resolvers": "npm:5.2.2"
13000+
"@marsidev/react-turnstile": "npm:1.4.2"
1301113001
"@polkadot/api": "npm:16.4.6"
1301213002
"@polkadot/react-identicon": "npm:3.16.4"
1301313003
"@polkadot/types": "npm:16.4.6"

0 commit comments

Comments
 (0)