diff --git a/.env.example b/.env.example index 70580001c8..1b06de63c5 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx -NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx diff --git a/configs/app/features/account.ts b/configs/app/features/account.ts index 56cf1d1eaa..82ae3b458b 100644 --- a/configs/app/features/account.ts +++ b/configs/app/features/account.ts @@ -6,11 +6,11 @@ import { getEnvValue } from '../utils'; const title = 'My account'; const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => { - if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV3.siteKey) { + if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV2.siteKey) { return Object.freeze({ title, isEnabled: true, - recaptchaSiteKey: services.reCaptchaV3.siteKey, + recaptchaSiteKey: services.reCaptchaV2.siteKey, }); } diff --git a/configs/app/features/csvExport.ts b/configs/app/features/csvExport.ts index 1eb2cbe289..fe1e24ec34 100644 --- a/configs/app/features/csvExport.ts +++ b/configs/app/features/csvExport.ts @@ -5,12 +5,12 @@ import services from '../services'; const title = 'Export data to CSV file'; const config: Feature<{ reCaptcha: { siteKey: string } }> = (() => { - if (services.reCaptchaV3.siteKey) { + if (services.reCaptchaV2.siteKey) { return Object.freeze({ title, isEnabled: true, reCaptcha: { - siteKey: services.reCaptchaV3.siteKey, + siteKey: services.reCaptchaV2.siteKey, }, }); } diff --git a/configs/app/features/publicTagsSubmission.ts b/configs/app/features/publicTagsSubmission.ts index ab1b0e33d2..6ba9ade0f0 100644 --- a/configs/app/features/publicTagsSubmission.ts +++ b/configs/app/features/publicTagsSubmission.ts @@ -9,7 +9,7 @@ const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); const title = 'Public tag submission'; const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { - if (services.reCaptchaV3.siteKey && addressMetadata.isEnabled && apiHost) { + if (services.reCaptchaV2.siteKey && addressMetadata.isEnabled && apiHost) { return Object.freeze({ title, isEnabled: true, diff --git a/configs/app/services.ts b/configs/app/services.ts index 1472f00f82..ee7d3a0b66 100644 --- a/configs/app/services.ts +++ b/configs/app/services.ts @@ -1,7 +1,7 @@ import { getEnvValue } from './utils'; export default Object.freeze({ - reCaptchaV3: { - siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY'), + reCaptchaV2: { + siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'), }, }); diff --git a/configs/envs/.env.jest b/configs/envs/.env.jest index 7de7ee11fc..abe2107a80 100644 --- a/configs/envs/.env.jest +++ b/configs/envs/.env.jest @@ -49,4 +49,4 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004 NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006 -NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx diff --git a/configs/envs/.env.pw b/configs/envs/.env.pw index e7be38baea..a984b9f6fb 100644 --- a/configs/envs/.env.pw +++ b/configs/envs/.env.pw @@ -52,7 +52,7 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006 NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007 NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008 -NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=tom diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts index c085ea2800..69e0c3c734 100644 --- a/deploy/tools/envs-validator/index.ts +++ b/deploy/tools/envs-validator/index.ts @@ -143,7 +143,7 @@ function printDeprecationWarning(envsMap: Record) { if (envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) { console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗'); // eslint-disable-next-line max-len - console.warn('The NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable.'); + console.warn('The NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable is now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY variable.'); console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n'); } @@ -182,9 +182,9 @@ function printDeprecationWarning(envsMap: Record) { function checkDeprecatedEnvs(envsMap: Record) { !silent && console.log(`🌀 Checking deprecated environment variables...`); - if (envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && !envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) { + if (!envsMap.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY && envsMap.NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY) { // eslint-disable-next-line max-len - console.log('🚨 The NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY variable is no longer supported. Please pass NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY or remove it completely.'); + console.log('🚨 The NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY variable is no longer supported. Please pass NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY or remove it completely.'); throw new Error(); } diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 355c67484e..05eb1c52d0 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -889,8 +889,8 @@ const schema = yup // 6. External services envs NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), - NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), // DEPRECATED - NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(), + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(), + NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(), // DEPRECATED NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(), NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(), diff --git a/deploy/tools/envs-validator/test/.env.alt b/deploy/tools/envs-validator/test/.env.alt index fe62f612d7..172a809a56 100644 --- a/deploy/tools/envs-validator/test/.env.alt +++ b/deploy/tools/envs-validator/test/.env.alt @@ -4,5 +4,5 @@ NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none NEXT_PUBLIC_HOMEPAGE_STATS=[] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo -NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=deprecated -NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx \ No newline at end of file +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx +NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated \ No newline at end of file diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 6f47b0afce..debb88fd4e 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -3,7 +3,7 @@ NEXT_PUBLIC_AUTH_URL=https://example.com NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_LOGOUT_URL=https://example.com NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx -NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx +NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx diff --git a/deploy/values/review-l2/values.yaml.gotmpl b/deploy/values/review-l2/values.yaml.gotmpl index 492ba76c9c..3a91738499 100644 --- a/deploy/values/review-l2/values.yaml.gotmpl +++ b/deploy/values/review-l2/values.yaml.gotmpl @@ -76,4 +76,4 @@ frontend: NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_OG_IMAGE_URL: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/base-mainnet.png - NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY diff --git a/deploy/values/review/values.yaml.gotmpl b/deploy/values/review/values.yaml.gotmpl index 30eda36084..6bd0759fa1 100644 --- a/deploy/values/review/values.yaml.gotmpl +++ b/deploy/values/review/values.yaml.gotmpl @@ -77,11 +77,12 @@ frontend: NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']" PROMETHEUS_METRICS_ENABLED: true + NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true envFromSecret: NEXT_PUBLIC_AUTH0_CLIENT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_AUTH0_CLIENT_ID NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: ref+vault://deployment-values/blockscout/dev/review?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN - NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY + NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN diff --git a/docs/DEPRECATED_ENVS.md b/docs/DEPRECATED_ENVS.md index 3b06de639f..171df270f9 100644 --- a/docs/DEPRECATED_ENVS.md +++ b/docs/DEPRECATED_ENVS.md @@ -12,4 +12,3 @@ | NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL | | NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS | -| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `` | v1.0.x+ | v1.36.0 | Replaced by NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | diff --git a/docs/ENVS.md b/docs/ENVS.md index 016ef289bc..19cb88d94d 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -349,7 +349,7 @@ Settings for meta tags, OG tags and SEO | Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | --- | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | Required | - | `true` | v1.0.x+ | -| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `` | v1.36.0+ | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `` | v1.0.x+ | | NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `` | v1.0.x+ | | NEXT_PUBLIC_AUTH_URL | `string` | **DEPRECATED** Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | - | - | `https://blockscout.com` | v1.0.x+ | | NEXT_PUBLIC_LOGOUT_URL | `string` | **DEPRECATED** Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` | v1.0.x+ | @@ -452,7 +452,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi | Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | --- | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `` | v1.36.0+ | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `` | v1.0.x+ |   @@ -614,6 +614,7 @@ This feature allows you to submit an application with a public address tag. | --- | --- | --- | --- | --- | --- | --- | | NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ | | NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` | v1.1.0+ | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `` | v1.0.x+ |   @@ -848,5 +849,5 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt | Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | --- | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | Google reCAPTCHA v3 site key | - | - | `` | v1.36.0+ | -| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | **DEPRECATED** Google reCAPTCHA v2 site key | - | - | `` | v1.0.x+ | +| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | **DEPRECATED** Google reCAPTCHA v3 site key | - | - | `` | v1.36.0+ | +| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `` | v1.0.x+ | \ No newline at end of file diff --git a/nextjs/csp/policies/googleReCaptcha.ts b/nextjs/csp/policies/googleReCaptcha.ts index 55826d250d..f7d0187523 100644 --- a/nextjs/csp/policies/googleReCaptcha.ts +++ b/nextjs/csp/policies/googleReCaptcha.ts @@ -3,7 +3,7 @@ import type CspDev from 'csp-dev'; import config from 'configs/app'; export function googleReCaptcha(): CspDev.DirectiveDescriptor { - if (!config.services.reCaptchaV3.siteKey) { + if (!config.services.reCaptchaV2.siteKey) { return {}; } diff --git a/package.json b/package.json index 04f5c49210..9152ce49e4 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "react": "18.3.1", "react-device-detect": "^2.2.3", "react-dom": "18.3.1", - "react-google-recaptcha-v3": "1.10.1", + "react-google-recaptcha": "3.1.0", "react-hook-form": "7.52.1", "react-identicons": "^1.2.5", "react-intersection-observer": "^9.5.2", diff --git a/playwright-ct.config.ts b/playwright-ct.config.ts index 5a13d4e9e3..306dd1eedd 100644 --- a/playwright-ct.config.ts +++ b/playwright-ct.config.ts @@ -78,6 +78,9 @@ const config: PlaywrightTestConfig = defineConfig({ // Mock for growthbook to test feature flags { find: 'lib/growthbook/useFeatureValue', replacement: './playwright/mocks/lib/growthbook/useFeatureValue.js' }, + // Mock for reCaptcha hook + { find: 'ui/shared/reCaptcha/useReCaptcha', replacement: './playwright/mocks/ui/shared/recaptcha/useReCaptcha.js' }, + // The createWeb3Modal() function from web3modal/wagmi/react somehow pollutes the global styles which causes the tests to fail // We don't call this function in TestApp and since we use useWeb3Modal() and useWeb3ModalState() hooks in the code, we have to mock the module // Otherwise it will complain that createWeb3Modal() is no called before the hooks are used diff --git a/playwright/mocks/ui/shared/recaptcha/useReCaptcha.js b/playwright/mocks/ui/shared/recaptcha/useReCaptcha.js new file mode 100644 index 0000000000..7d4b9de477 --- /dev/null +++ b/playwright/mocks/ui/shared/recaptcha/useReCaptcha.js @@ -0,0 +1,8 @@ +const useReCaptcha = () => { + return { + ref: { current: null }, + executeAsync: () => Promise.resolve('recaptcha_token'), + }; +}; + +export default useReCaptcha; diff --git a/theme/globals/recaptcha.ts b/theme/globals/recaptcha.ts index 0db7a295a2..c551eb4ae2 100644 --- a/theme/globals/recaptcha.ts +++ b/theme/globals/recaptcha.ts @@ -1,7 +1,20 @@ const styles = () => { return { '.grecaptcha-badge': { - zIndex: 'toast', + visibility: 'hidden', + }, + 'div:has(div):has(iframe[title="recaptcha challenge expires in two minutes"])': { + '&::after': { + content: `" "`, + display: 'block', + position: 'fixed', + top: 0, + left: 0, + width: '100vw', + height: '100vh', + zIndex: 100000, + bgColor: 'blackAlpha.300', + }, }, }; }; diff --git a/ui/csvExport/CsvExportForm.tsx b/ui/csvExport/CsvExportForm.tsx index bbde5b3185..12c85568f5 100644 --- a/ui/csvExport/CsvExportForm.tsx +++ b/ui/csvExport/CsvExportForm.tsx @@ -1,6 +1,5 @@ import { Alert, Button, chakra, Flex } from '@chakra-ui/react'; import React from 'react'; -import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; import type { SubmitHandler } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form'; @@ -13,7 +12,8 @@ import type { ResourceName } from 'lib/api/resources'; import dayjs from 'lib/date/dayjs'; import downloadBlob from 'lib/downloadBlob'; import useToast from 'lib/hooks/useToast'; -import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha'; +import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha'; +import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha'; import CsvExportFormField from './CsvExportFormField'; @@ -36,16 +36,23 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla }); const { handleSubmit, formState } = formApi; const toast = useToast(); + const recaptcha = useReCaptcha(); const onFormSubmit: SubmitHandler = React.useCallback(async(data) => { try { + const token = await recaptcha.executeAsync(); + + if (!token) { + throw new Error('ReCaptcha is not solved'); + } + const url = buildUrl(resource, { hash } as never, { address_id: hash, from_period: exportType !== 'holders' ? data.from : null, to_period: exportType !== 'holders' ? data.to : null, filter_type: filterType, filter_value: filterValue, - recaptcha_v3_response: data.reCaptcha, + recaptcha_response: token, }); const response = await fetch(url, { @@ -76,9 +83,9 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla }); } - }, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]); + }, [ recaptcha, resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]); - if (!config.services.reCaptchaV3.siteKey) { + if (!config.services.reCaptchaV2.siteKey) { return ( CSV export is not available at the moment since reCaptcha is not configured for this application. @@ -88,31 +95,29 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla } return ( - - - + + + { exportType !== 'holders' && } + { exportType !== 'holders' && } + + + - - - + Download + + + ); }; diff --git a/ui/csvExport/types.ts b/ui/csvExport/types.ts index 6f8effb629..ca5ec6a0c2 100644 --- a/ui/csvExport/types.ts +++ b/ui/csvExport/types.ts @@ -1,5 +1,4 @@ export interface FormFields { from: string; to: string; - reCaptcha: string; } diff --git a/ui/myProfile/MyProfileEmail.tsx b/ui/myProfile/MyProfileEmail.tsx index 0a2e48d45e..345dd42d70 100644 --- a/ui/myProfile/MyProfileEmail.tsx +++ b/ui/myProfile/MyProfileEmail.tsx @@ -1,7 +1,6 @@ import { Button, chakra, Heading, useDisclosure } from '@chakra-ui/react'; import type { UseQueryResult } from '@tanstack/react-query'; import React from 'react'; -import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; import type { SubmitHandler } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form'; @@ -14,8 +13,9 @@ import getErrorMessage from 'lib/errors/getErrorMessage'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import useToast from 'lib/hooks/useToast'; import * as mixpanel from 'lib/mixpanel'; -import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha'; import FormFieldText from 'ui/shared/forms/fields/FormFieldText'; +import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha'; +import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha'; import AuthModal from 'ui/snippets/auth/AuthModal'; import MyProfileFieldsEmail from './fields/MyProfileFieldsEmail'; @@ -34,6 +34,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => { const authModal = useDisclosure(); const apiFetch = useApiFetch(); const toast = useToast(); + const recaptcha = useReCaptcha(); const formApi = useForm({ mode: 'onBlur', @@ -45,12 +46,14 @@ const MyProfileEmail = ({ profileQuery }: Props) => { const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { try { + const token = await recaptcha.executeAsync(); + await apiFetch('auth_send_otp', { fetchParams: { method: 'POST', body: { email: formData.email, - recaptcha_v3_response: formData.reCaptcha, + recaptcha_response: token, }, }, }); @@ -68,7 +71,7 @@ const MyProfileEmail = ({ profileQuery }: Props) => { description: apiError?.message || getErrorMessage(error) || 'Something went wrong', }); } - }, [ apiFetch, authModal, toast ]); + }, [ apiFetch, authModal, toast, recaptcha ]); const hasDirtyFields = Object.keys(formApi.formState.dirtyFields).length > 0; @@ -82,15 +85,11 @@ const MyProfileEmail = ({ profileQuery }: Props) => { > name="name" placeholder="Name" isReadOnly mb={ 3 }/> - { config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && ( - - - - ) } - { config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && ( + { config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && } + { config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && ( - - - - + Send request + + + + ); }; diff --git a/ui/publicTags/submit/types.ts b/ui/publicTags/submit/types.ts index 5b0871ae68..4f55f9043b 100644 --- a/ui/publicTags/submit/types.ts +++ b/ui/publicTags/submit/types.ts @@ -9,7 +9,6 @@ export interface FormFields { addresses: Array<{ hash: string }>; tags: Array; description: string | undefined; - reCaptcha: string; } export interface FormFieldTag { diff --git a/ui/publicTags/submit/utils.test.ts b/ui/publicTags/submit/utils.test.ts index 1fb474808b..a13479db1d 100644 --- a/ui/publicTags/submit/utils.test.ts +++ b/ui/publicTags/submit/utils.test.ts @@ -5,7 +5,6 @@ describe('function convertFormDataToRequestsBody()', () => { it('should convert form data to requests body', () => { const formData = { ...mocks.baseFields, - reCaptcha: 'xxx', addresses: [ { hash: mocks.address1 }, { hash: mocks.address2 } ], tags: [ convertTagApiFieldsToFormFields(mocks.tag1), convertTagApiFieldsToFormFields(mocks.tag2) ], }; diff --git a/ui/shared/AppError/custom/AppErrorTooManyRequests.tsx b/ui/shared/AppError/custom/AppErrorTooManyRequests.tsx index 3600f79ae7..87d06ef625 100644 --- a/ui/shared/AppError/custom/AppErrorTooManyRequests.tsx +++ b/ui/shared/AppError/custom/AppErrorTooManyRequests.tsx @@ -1,11 +1,12 @@ import { Button, Text } from '@chakra-ui/react'; import React from 'react'; -import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; import config from 'configs/app'; import buildUrl from 'lib/api/buildUrl'; import useFetch from 'lib/hooks/useFetch'; import useToast from 'lib/hooks/useToast'; +import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha'; +import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha'; import AppErrorIcon from '../AppErrorIcon'; import AppErrorTitle from '../AppErrorTitle'; @@ -13,19 +14,16 @@ import AppErrorTitle from '../AppErrorTitle'; const AppErrorTooManyRequests = () => { const toast = useToast(); const fetch = useFetch(); - const [ token, setToken ] = React.useState(undefined); - - const handleReCaptchaChange = React.useCallback(async(token: string) => { - setToken(token); - }, [ ]); + const recaptcha = useReCaptcha(); const handleSubmit = React.useCallback(async() => { try { + const token = await recaptcha.executeAsync(); const url = buildUrl('api_v2_key'); await fetch(url, { method: 'POST', - body: { recaptcha_v3_response: token }, + body: { recaptcha_response: token }, credentials: 'include', }, { resource: 'api_v2_key', @@ -43,25 +41,22 @@ const AppErrorTooManyRequests = () => { isClosable: true, }); } - }, [ token, toast, fetch ]); + }, [ recaptcha, toast, fetch ]); - if (!config.services.reCaptchaV3.siteKey) { - throw new Error('reCAPTCHA V3 site key is not set'); + if (!config.services.reCaptchaV2.siteKey) { + throw new Error('reCAPTCHA V2 site key is not set'); } return ( - + <> You have exceeded the request rate for a given time period. Please reduce the number of requests and try again soon. - + - + ); }; diff --git a/ui/shared/forms/fields/FormFieldReCaptcha.tsx b/ui/shared/forms/fields/FormFieldReCaptcha.tsx deleted file mode 100644 index 38de955903..0000000000 --- a/ui/shared/forms/fields/FormFieldReCaptcha.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { GoogleReCaptcha } from 'react-google-recaptcha-v3'; -import { useFormContext } from 'react-hook-form'; - -const FormFieldReCaptcha = () => { - - const { register, unregister, clearErrors, setValue, formState } = useFormContext(); - - React.useEffect(() => { - register('reCaptcha', { required: true, shouldUnregister: true }); - - return () => { - unregister('reCaptcha'); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleReCaptchaChange = React.useCallback((token: string) => { - clearErrors('reCaptcha'); - setValue('reCaptcha', token, { shouldValidate: true }); - }, [ clearErrors, setValue ]); - - return ( - - ); -}; - -export default FormFieldReCaptcha; diff --git a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png index d1397d9773..b3bb7d5b65 100644 Binary files a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-horizontal-navigation-1.png b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-horizontal-navigation-1.png index bdd52af780..acadfd83c5 100644 Binary files a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-horizontal-navigation-1.png and b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-horizontal-navigation-1.png differ diff --git a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-vertical-navigation-1.png b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-vertical-navigation-1.png index ab7784ee6c..99318f6b9c 100644 Binary files a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-vertical-navigation-1.png and b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_xxl-screen-vertical-navigation-1.png differ diff --git a/ui/shared/layout/__screenshots__/Layout.pw.tsx_mobile_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/Layout.pw.tsx_mobile_base-view-mobile-1.png index dc714c2ff3..def277edeb 100644 Binary files a/ui/shared/layout/__screenshots__/Layout.pw.tsx_mobile_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/Layout.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png index 471275bb7f..179b29d536 100644 Binary files a/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_mobile_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_mobile_base-view-mobile-1.png index 0ba1d2c0ac..46f3f96442 100644 Binary files a/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_mobile_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png index 0fa3ec2d03..923b92e37d 100644 Binary files a/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_mobile_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_mobile_base-view-mobile-1.png index 687bb12c06..121a30044c 100644 Binary files a/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_mobile_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/shared/reCaptcha/ReCaptcha.tsx b/ui/shared/reCaptcha/ReCaptcha.tsx new file mode 100644 index 0000000000..83152d7266 --- /dev/null +++ b/ui/shared/reCaptcha/ReCaptcha.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import ReCaptcha from 'react-google-recaptcha'; + +import config from 'configs/app'; + +interface Props { + disabledFeatureMessage?: React.ReactNode; +} + +const ReCaptchaInvisible = ({ disabledFeatureMessage }: Props, ref: React.Ref) => { + if (!config.services.reCaptchaV2.siteKey) { + return disabledFeatureMessage ?? null; + } + + return ( + + ); +}; + +export default React.forwardRef(ReCaptchaInvisible); diff --git a/ui/shared/reCaptcha/useReCaptcha.tsx b/ui/shared/reCaptcha/useReCaptcha.tsx new file mode 100644 index 0000000000..f4a5713854 --- /dev/null +++ b/ui/shared/reCaptcha/useReCaptcha.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import type ReCAPTCHA from 'react-google-recaptcha'; + +export default function useReCaptcha() { + const ref = React.useRef(null); + const rejectCb = React.useRef<((error: Error) => void) | null>(null); + + const [ isOpen, setIsOpen ] = React.useState(false); + + const executeAsync = React.useCallback(async() => { + setIsOpen(true); + const tokenPromise = ref.current?.executeAsync() || Promise.reject(new Error('Unable to execute ReCaptcha')); + const modalOpenPromise = new Promise((resolve, reject) => { + rejectCb.current = reject; + }); + + return Promise.race([ tokenPromise, modalOpenPromise ]); + }, [ ref ]); + + const handleContainerClick = React.useCallback(() => { + setIsOpen(false); + rejectCb.current?.(new Error('ReCaptcha is not solved')); + }, []); + + React.useEffect(() => { + if (!isOpen) { + return; + } + + const container = window.document.querySelector('div:has(div):has(iframe[title="recaptcha challenge expires in two minutes"])'); + container?.addEventListener('click', handleContainerClick); + + return () => { + container?.removeEventListener('click', handleContainerClick); + }; + }, [ isOpen, handleContainerClick ]); + + return React.useMemo(() => ({ ref, executeAsync }), [ ref, executeAsync ]); +} diff --git a/ui/snippets/auth/AuthModal.tsx b/ui/snippets/auth/AuthModal.tsx index 6c532ce1ca..99ecf0cad9 100644 --- a/ui/snippets/auth/AuthModal.tsx +++ b/ui/snippets/auth/AuthModal.tsx @@ -2,7 +2,6 @@ import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOve import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import React from 'react'; -import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; import type { Screen, ScreenSuccess } from './types'; @@ -183,9 +182,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro - - { content } - + { content } diff --git a/ui/snippets/auth/screens/AuthModalScreenEmail.tsx b/ui/snippets/auth/screens/AuthModalScreenEmail.tsx index f96904a650..51c959e289 100644 --- a/ui/snippets/auth/screens/AuthModalScreenEmail.tsx +++ b/ui/snippets/auth/screens/AuthModalScreenEmail.tsx @@ -1,6 +1,5 @@ import { chakra, Button, Text } from '@chakra-ui/react'; import React from 'react'; -import { useGoogleReCaptcha } from 'react-google-recaptcha-v3'; import type { SubmitHandler } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form'; @@ -12,6 +11,8 @@ import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import useToast from 'lib/hooks/useToast'; import * as mixpanel from 'lib/mixpanel'; import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail'; +import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha'; +import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha'; interface Props { onSubmit: (screen: Screen) => void; @@ -27,7 +28,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { const apiFetch = useApiFetch(); const toast = useToast(); - const { executeRecaptcha } = useGoogleReCaptcha(); + const recaptcha = useReCaptcha(); const formApi = useForm({ mode: 'onBlur', @@ -38,13 +39,14 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { const onFormSubmit: SubmitHandler = React.useCallback(async(formData) => { try { - const token = await executeRecaptcha?.(); + const token = await recaptcha.executeAsync(); + await apiFetch('auth_send_otp', { fetchParams: { method: 'POST', body: { email: formData.email, - recaptcha_v3_response: token, + recaptcha_response: token, }, }, }); @@ -68,7 +70,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { description: getErrorObjPayload<{ message: string }>(error)?.message || getErrorMessage(error) || 'Something went wrong', }); } - }, [ executeRecaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]); + }, [ recaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]); return ( @@ -93,6 +95,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { > Send a code + ); diff --git a/ui/snippets/auth/screens/AuthModalScreenOtpCode.tsx b/ui/snippets/auth/screens/AuthModalScreenOtpCode.tsx index 73e7047c79..87cd2ec9fd 100644 --- a/ui/snippets/auth/screens/AuthModalScreenOtpCode.tsx +++ b/ui/snippets/auth/screens/AuthModalScreenOtpCode.tsx @@ -1,6 +1,5 @@ import { chakra, Box, Text, Button } from '@chakra-ui/react'; import React from 'react'; -import { useGoogleReCaptcha } from 'react-google-recaptcha-v3'; import type { SubmitHandler } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form'; @@ -12,6 +11,8 @@ import getErrorMessage from 'lib/errors/getErrorMessage'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import useToast from 'lib/hooks/useToast'; import IconSvg from 'ui/shared/IconSvg'; +import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha'; +import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha'; import AuthModalFieldOtpCode from '../fields/AuthModalFieldOtpCode'; @@ -25,7 +26,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { const apiFetch = useApiFetch(); const toast = useToast(); - const { executeRecaptcha } = useGoogleReCaptcha(); + const recaptcha = useReCaptcha(); const [ isCodeSending, setIsCodeSending ] = React.useState(false); const formApi = useForm({ @@ -72,11 +73,11 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { try { formApi.clearErrors('code'); setIsCodeSending(true); - const token = await executeRecaptcha?.(); + const token = await recaptcha.executeAsync(); await apiFetch('auth_send_otp', { fetchParams: { method: 'POST', - body: { email, recaptcha_v3_response: token }, + body: { email, recaptcha_response: token }, }, }); @@ -96,7 +97,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { } finally { setIsCodeSending(false); } - }, [ apiFetch, email, executeRecaptcha, formApi, toast ]); + }, [ apiFetch, email, formApi, toast, recaptcha ]); return ( @@ -110,6 +111,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { and enter your code below. +