ReCaptcha: migrate back to v2 solution (#2446)
* show recaptcha in the site footer * replace badge in footer with text * add invisible reCaptcha v2 * handle unsolved reCaptcha case * migrate the rest of components to invisible reCaptcha v2 * remove unused code * refactoring * add env for demo * fix tests * update values for demo * update link to secret in vault * mock recaptcha for tests * ohhh victor victor * fix token metadata update test * deprecate variable for ReCaptcha v3
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -143,7 +143,7 @@ function printDeprecationWarning(envsMap: Record<string, string>) {
|
||||
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<string, string>) {
|
||||
function checkDeprecatedEnvs(envsMap: Record<string, string>) {
|
||||
!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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 | - | - | `<your-secret>` | v1.0.x+ | v1.36.0 | Replaced by NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY |
|
||||
|
||||
@@ -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 | - | `<your-secret>` | v1.36.0+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `boolean` | See [below](ENVS.md#google-recaptcha) | Required | - | `<your-secret>` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | **DEPRECATED** Client id for [Auth0](https://auth0.com/) provider | - | - | `<your-secret>` | 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 | - | `<your-secret>` | v1.36.0+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | 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 | - | `<your-secret>` | 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 | - | - | `<your-secret>` | v1.36.0+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | **DEPRECATED** Google reCAPTCHA v2 site key | - | - | `<your-secret>` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | **DEPRECATED** Google reCAPTCHA v3 site key | - | - | `<your-secret>` | v1.36.0+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Google reCAPTCHA v2 site key | - | - | `<your-secret>` | v1.0.x+ |
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const useReCaptcha = () => {
|
||||
return {
|
||||
ref: { current: null },
|
||||
executeAsync: () => Promise.resolve('recaptcha_token'),
|
||||
};
|
||||
};
|
||||
|
||||
export default useReCaptcha;
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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<FormFields> = 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 (
|
||||
<Alert status="error">
|
||||
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 (
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
|
||||
<FormProvider { ...formApi }>
|
||||
<chakra.form
|
||||
noValidate
|
||||
onSubmit={ handleSubmit(onFormSubmit) }
|
||||
<FormProvider { ...formApi }>
|
||||
<chakra.form
|
||||
noValidate
|
||||
onSubmit={ handleSubmit(onFormSubmit) }
|
||||
>
|
||||
<Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap">
|
||||
{ exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> }
|
||||
{ exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> }
|
||||
</Flex>
|
||||
<ReCaptcha ref={ recaptcha.ref }/>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="lg"
|
||||
type="submit"
|
||||
mt={ 8 }
|
||||
isLoading={ formState.isSubmitting }
|
||||
loadingText="Download"
|
||||
isDisabled={ Boolean(formState.errors.from || formState.errors.to) }
|
||||
>
|
||||
<Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap">
|
||||
{ exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> }
|
||||
{ exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> }
|
||||
<FormFieldReCaptcha/>
|
||||
</Flex>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="lg"
|
||||
type="submit"
|
||||
mt={ 8 }
|
||||
isLoading={ formState.isSubmitting }
|
||||
loadingText="Download"
|
||||
isDisabled={ Boolean(formState.errors.from || formState.errors.to) }
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
</GoogleReCaptchaProvider>
|
||||
Download
|
||||
</Button>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface FormFields {
|
||||
from: string;
|
||||
to: string;
|
||||
reCaptcha: string;
|
||||
}
|
||||
|
||||
@@ -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<FormFields>({
|
||||
mode: 'onBlur',
|
||||
@@ -45,12 +46,14 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
|
||||
|
||||
const onFormSubmit: SubmitHandler<FormFields> = 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) => {
|
||||
>
|
||||
<FormFieldText<FormFields> name="name" placeholder="Name" isReadOnly mb={ 3 }/>
|
||||
<MyProfileFieldsEmail
|
||||
isReadOnly={ !config.services.reCaptchaV3.siteKey || Boolean(profileQuery.data?.email) }
|
||||
isReadOnly={ !config.services.reCaptchaV2.siteKey || Boolean(profileQuery.data?.email) }
|
||||
defaultValue={ profileQuery.data?.email || undefined }
|
||||
/>
|
||||
{ config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && (
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
|
||||
<FormFieldReCaptcha/>
|
||||
</GoogleReCaptchaProvider>
|
||||
) }
|
||||
{ config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && (
|
||||
{ config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && <ReCaptcha ref={ recaptcha.ref }/> }
|
||||
{ config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && (
|
||||
<Button
|
||||
mt={ 6 }
|
||||
size="sm"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface FormFields {
|
||||
email: string;
|
||||
name: string;
|
||||
reCaptcha: string;
|
||||
}
|
||||
|
||||
@@ -62,12 +62,6 @@ test('metadata update', async({ render, page, createSocket, mockApiResponse, moc
|
||||
// open the menu, click the button and submit form
|
||||
await page.getByLabel('Address menu').click();
|
||||
await page.getByRole('menuitem', { name: 'Refresh metadata' }).click();
|
||||
await page.evaluate(() => {
|
||||
const form = document.querySelector('form');
|
||||
form && (form.style.display = 'block');
|
||||
});
|
||||
await page.getByPlaceholder('reCaptcha token').fill('xxx');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// join socket channel
|
||||
const channel = await socketServer.joinChannel(socket, `token_instances:${ hash.toLowerCase() }`);
|
||||
@@ -116,12 +110,6 @@ test('metadata update failed', async({ render, page }) => {
|
||||
// open the menu, click the button and submit form
|
||||
await page.getByLabel('Address menu').click();
|
||||
await page.getByRole('menuitem', { name: 'Refresh metadata' }).click();
|
||||
await page.evaluate(() => {
|
||||
const form = document.querySelector('form');
|
||||
form && (form.style.display = 'block');
|
||||
});
|
||||
await page.getByPlaceholder('reCaptcha token').fill('xxx');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// check that button is not disabled
|
||||
await page.getByLabel('Address menu').click();
|
||||
|
||||
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 196 KiB |
@@ -1,7 +1,6 @@
|
||||
import { Button, chakra, Grid, GridItem } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
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';
|
||||
|
||||
@@ -15,10 +14,11 @@ import getErrorObj from 'lib/errors/getErrorObj';
|
||||
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import FormFieldEmail from 'ui/shared/forms/fields/FormFieldEmail';
|
||||
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
|
||||
import FormFieldText from 'ui/shared/forms/fields/FormFieldText';
|
||||
import FormFieldUrl from 'ui/shared/forms/fields/FormFieldUrl';
|
||||
import Hint from 'ui/shared/Hint';
|
||||
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
|
||||
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
|
||||
|
||||
import PublicTagsSubmitFieldAddresses from './fields/PublicTagsSubmitFieldAddresses';
|
||||
import PublicTagsSubmitFieldTags from './fields/PublicTagsSubmitFieldTags';
|
||||
@@ -34,6 +34,7 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const router = useRouter();
|
||||
const apiFetch = useApiFetch();
|
||||
const recaptcha = useReCaptcha();
|
||||
|
||||
const formApi = useForm<FormFields>({
|
||||
mode: 'onBlur',
|
||||
@@ -56,13 +57,16 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
|
||||
const requestsBody = convertFormDataToRequestsBody(data);
|
||||
|
||||
const result = await Promise.all(requestsBody.map(async(body) => {
|
||||
return apiFetch<'public_tag_application', unknown, { message: string }>('public_tag_application', {
|
||||
pathParams: { chainId: appConfig.chain.id },
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { submission: body },
|
||||
},
|
||||
})
|
||||
return recaptcha.executeAsync()
|
||||
.then(() => {
|
||||
return apiFetch<'public_tag_application', unknown, { message: string }>('public_tag_application', {
|
||||
pathParams: { chainId: appConfig.chain.id },
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { submission: body },
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => ({ error: null, payload: body }))
|
||||
.catch((error: unknown) => {
|
||||
const errorObj = getErrorObj(error);
|
||||
@@ -74,9 +78,9 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
|
||||
}));
|
||||
|
||||
onSubmitResult(result);
|
||||
}, [ apiFetch, onSubmitResult ]);
|
||||
}, [ apiFetch, onSubmitResult, recaptcha ]);
|
||||
|
||||
if (!appConfig.services.reCaptchaV3.siteKey) {
|
||||
if (!appConfig.services.reCaptchaV2.siteKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -85,69 +89,67 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ appConfig.services.reCaptchaV3.siteKey }>
|
||||
<FormProvider { ...formApi }>
|
||||
<chakra.form
|
||||
noValidate
|
||||
onSubmit={ formApi.handleSubmit(onFormSubmit) }
|
||||
<FormProvider { ...formApi }>
|
||||
<chakra.form
|
||||
noValidate
|
||||
onSubmit={ formApi.handleSubmit(onFormSubmit) }
|
||||
>
|
||||
<Grid
|
||||
columnGap={ 3 }
|
||||
rowGap={ 3 }
|
||||
templateColumns={{ base: '1fr', lg: '1fr 1fr minmax(0, 200px)', xl: '1fr 1fr minmax(0, 250px)' }}
|
||||
>
|
||||
<Grid
|
||||
columnGap={ 3 }
|
||||
rowGap={ 3 }
|
||||
templateColumns={{ base: '1fr', lg: '1fr 1fr minmax(0, 200px)', xl: '1fr 1fr minmax(0, 250px)' }}
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4">
|
||||
Company info
|
||||
</GridItem>
|
||||
<FormFieldText<FormFields> name="requesterName" isRequired placeholder="Your name" { ...fieldProps }/>
|
||||
<FormFieldEmail<FormFields> name="requesterEmail" isRequired { ...fieldProps }/>
|
||||
|
||||
{ !isMobile && <div/> }
|
||||
<FormFieldText<FormFields> name="companyName" placeholder="Company name" { ...fieldProps }/>
|
||||
<FormFieldUrl<FormFields> name="companyWebsite" placeholder="Company website" { ...fieldProps }/>
|
||||
{ !isMobile && <div/> }
|
||||
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}>
|
||||
Public tags/labels
|
||||
<Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link"/>
|
||||
</GridItem>
|
||||
<PublicTagsSubmitFieldAddresses/>
|
||||
<PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/>
|
||||
<GridItem colSpan={{ base: 1, lg: 2 }}>
|
||||
<FormFieldText<FormFields>
|
||||
name="description"
|
||||
isRequired
|
||||
placeholder={
|
||||
isMobile ?
|
||||
'Confirm the connection between addresses and tags.' :
|
||||
'Provide a comment to confirm the connection between addresses and tags.'
|
||||
}
|
||||
maxH="160px"
|
||||
rules={{ maxLength: 80 }}
|
||||
asComponent="Textarea"
|
||||
{ ...fieldProps }
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }}>
|
||||
<ReCaptcha ref={ recaptcha.ref }/>
|
||||
</GridItem>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
size="lg"
|
||||
type="submit"
|
||||
mt={ 3 }
|
||||
isLoading={ formApi.formState.isSubmitting }
|
||||
loadingText="Send request"
|
||||
w="min-content"
|
||||
>
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4">
|
||||
Company info
|
||||
</GridItem>
|
||||
<FormFieldText<FormFields> name="requesterName" isRequired placeholder="Your name" { ...fieldProps }/>
|
||||
<FormFieldEmail<FormFields> name="requesterEmail" isRequired { ...fieldProps }/>
|
||||
|
||||
{ !isMobile && <div/> }
|
||||
<FormFieldText<FormFields> name="companyName" placeholder="Company name" { ...fieldProps }/>
|
||||
<FormFieldUrl<FormFields> name="companyWebsite" placeholder="Company website" { ...fieldProps }/>
|
||||
{ !isMobile && <div/> }
|
||||
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}>
|
||||
Public tags/labels
|
||||
<Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link"/>
|
||||
</GridItem>
|
||||
<PublicTagsSubmitFieldAddresses/>
|
||||
<PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/>
|
||||
<GridItem colSpan={{ base: 1, lg: 2 }}>
|
||||
<FormFieldText<FormFields>
|
||||
name="description"
|
||||
isRequired
|
||||
placeholder={
|
||||
isMobile ?
|
||||
'Confirm the connection between addresses and tags.' :
|
||||
'Provide a comment to confirm the connection between addresses and tags.'
|
||||
}
|
||||
maxH="160px"
|
||||
rules={{ maxLength: 80 }}
|
||||
asComponent="Textarea"
|
||||
{ ...fieldProps }
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }}>
|
||||
<FormFieldReCaptcha/>
|
||||
</GridItem>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
size="lg"
|
||||
type="submit"
|
||||
mt={ 3 }
|
||||
isLoading={ formApi.formState.isSubmitting }
|
||||
loadingText="Send request"
|
||||
w="min-content"
|
||||
>
|
||||
Send request
|
||||
</Button>
|
||||
</Grid>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
</GoogleReCaptchaProvider>
|
||||
Send request
|
||||
</Button>
|
||||
</Grid>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ export interface FormFields {
|
||||
addresses: Array<{ hash: string }>;
|
||||
tags: Array<FormFieldTag>;
|
||||
description: string | undefined;
|
||||
reCaptcha: string;
|
||||
}
|
||||
|
||||
export interface FormFieldTag {
|
||||
|
||||
@@ -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) ],
|
||||
};
|
||||
|
||||
@@ -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<string | undefined>(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 (
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
|
||||
<>
|
||||
<AppErrorIcon statusCode={ 429 }/>
|
||||
<AppErrorTitle title="Too many requests"/>
|
||||
<Text variant="secondary" mt={ 3 }>
|
||||
You have exceeded the request rate for a given time period. Please reduce the number of requests and try again soon.
|
||||
</Text>
|
||||
<GoogleReCaptcha
|
||||
onVerify={ handleReCaptchaChange }
|
||||
refreshReCaptcha
|
||||
/>
|
||||
<ReCaptcha ref={ recaptcha.ref }/>
|
||||
<Button onClick={ handleSubmit } mt={ 8 }>Try again</Button>
|
||||
</GoogleReCaptchaProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<GoogleReCaptcha
|
||||
onVerify={ handleReCaptchaChange }
|
||||
refreshReCaptcha={ formState.submitCount ?? -1 }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormFieldReCaptcha;
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 59 KiB |
@@ -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<ReCaptcha>) => {
|
||||
if (!config.services.reCaptchaV2.siteKey) {
|
||||
return disabledFeatureMessage ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReCaptcha
|
||||
ref={ ref }
|
||||
sitekey={ config.services.reCaptchaV2.siteKey }
|
||||
size="invisible"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.forwardRef(ReCaptchaInvisible);
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import type ReCAPTCHA from 'react-google-recaptcha';
|
||||
|
||||
export default function useReCaptcha() {
|
||||
const ref = React.useRef<ReCAPTCHA>(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<void>((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 ]);
|
||||
}
|
||||
@@ -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
|
||||
</ModalHeader>
|
||||
<ModalCloseButton top={ 6 } right={ 6 } color="gray.400"/>
|
||||
<ModalBody mb={ 0 }>
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ feature.recaptchaSiteKey }>
|
||||
{ content }
|
||||
</GoogleReCaptchaProvider>
|
||||
{ content }
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -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<EmailFormFields>({
|
||||
mode: 'onBlur',
|
||||
@@ -38,13 +39,14 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
|
||||
const onFormSubmit: SubmitHandler<EmailFormFields> = 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 (
|
||||
<FormProvider { ...formApi }>
|
||||
@@ -93,6 +95,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
>
|
||||
Send a code
|
||||
</Button>
|
||||
<ReCaptcha ref={ recaptcha.ref }/>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
);
|
||||
|
||||
@@ -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<OtpCodeFormFields>({
|
||||
@@ -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 (
|
||||
<FormProvider { ...formApi }>
|
||||
@@ -110,6 +111,7 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
|
||||
and enter your code below.
|
||||
</Text>
|
||||
<AuthModalFieldOtpCode isDisabled={ isCodeSending }/>
|
||||
<ReCaptcha ref={ recaptcha.ref }/>
|
||||
<Button
|
||||
variant="link"
|
||||
display="flex"
|
||||
|
||||
@@ -164,6 +164,22 @@ const Footer = () => {
|
||||
m: '0 auto',
|
||||
};
|
||||
|
||||
const renderRecaptcha = (gridArea?: GridProps['gridArea']) => {
|
||||
if (!config.services.reCaptchaV2.siteKey) {
|
||||
return <Box gridArea={ gridArea }/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box gridArea={ gridArea } fontSize="xs" lineHeight={ 5 } mt={ 6 } color="text">
|
||||
<span>This site is protected by reCAPTCHA and the Google </span>
|
||||
<Link href="https://policies.google.com/privacy" isExternal>Privacy Policy</Link>
|
||||
<span> and </span>
|
||||
<Link href="https://policies.google.com/terms" isExternal>Terms of Service</Link>
|
||||
<span> apply.</span>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
if (config.UI.footer.links) {
|
||||
return (
|
||||
<Box { ...containerProps }>
|
||||
@@ -171,6 +187,7 @@ const Footer = () => {
|
||||
<div>
|
||||
{ renderNetworkInfo() }
|
||||
{ renderProjectInfo() }
|
||||
{ renderRecaptcha() }
|
||||
</div>
|
||||
|
||||
<Grid
|
||||
@@ -212,12 +229,14 @@ const Footer = () => {
|
||||
lg: `
|
||||
"network links-top"
|
||||
"info links-bottom"
|
||||
"recaptcha links-bottom"
|
||||
`,
|
||||
}}
|
||||
>
|
||||
|
||||
{ renderNetworkInfo({ lg: 'network' }) }
|
||||
{ renderProjectInfo({ lg: 'info' }) }
|
||||
{ renderRecaptcha({ lg: 'recaptcha' }) }
|
||||
|
||||
<Grid
|
||||
gridArea={{ lg: 'links-bottom' }}
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 41 KiB |
@@ -1,8 +1,7 @@
|
||||
import type { ToastId } from '@chakra-ui/react';
|
||||
import { chakra, Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner, Center } from '@chakra-ui/react';
|
||||
import { Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner, Center } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
|
||||
import type { SocketMessage } from 'lib/socket/types';
|
||||
import type { TokenInstance } from 'types/api/token';
|
||||
@@ -11,9 +10,12 @@ import config from 'configs/app';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import { MINUTE, SECOND } from 'lib/consts';
|
||||
import getErrorMessage from 'lib/errors/getErrorMessage';
|
||||
import useToast from 'lib/hooks/useToast';
|
||||
import useSocketChannel from 'lib/socket/useSocketChannel';
|
||||
import useSocketMessage from 'lib/socket/useSocketMessage';
|
||||
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
|
||||
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
|
||||
|
||||
import { useMetadataUpdateContext } from './contexts/metadataUpdate';
|
||||
|
||||
@@ -30,6 +32,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
const apiFetch = useApiFetch();
|
||||
const toast = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const recaptcha = useReCaptcha();
|
||||
|
||||
const handleRefreshError = React.useCallback(() => {
|
||||
setStatus?.('ERROR');
|
||||
@@ -42,53 +45,41 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
});
|
||||
}, [ setStatus, toast ]);
|
||||
|
||||
const initializeUpdate = React.useCallback((reCaptchaToken: string) => {
|
||||
apiFetch<'token_instance_refresh_metadata', unknown, unknown>('token_instance_refresh_metadata', {
|
||||
pathParams: { hash, id },
|
||||
fetchParams: {
|
||||
method: 'PATCH',
|
||||
body: { recaptcha_v3_response: reCaptchaToken },
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setStatus?.('WAITING_FOR_RESPONSE');
|
||||
toastId.current = toast({
|
||||
title: 'Please wait',
|
||||
description: 'Refetching metadata request sent',
|
||||
icon: <Spinner size="sm" mr={ 2 }/>,
|
||||
status: 'warning',
|
||||
duration: null,
|
||||
isClosable: false,
|
||||
});
|
||||
timeoutId.current = window.setTimeout(handleRefreshError, 2 * MINUTE);
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Unable to initialize metadata update',
|
||||
status: 'warning',
|
||||
});
|
||||
setStatus?.('ERROR');
|
||||
const initializeUpdate = React.useCallback(async(tokenProp?: string) => {
|
||||
try {
|
||||
const token = tokenProp || await recaptcha.executeAsync();
|
||||
await apiFetch<'token_instance_refresh_metadata', unknown, unknown>('token_instance_refresh_metadata', {
|
||||
pathParams: { hash, id },
|
||||
fetchParams: {
|
||||
method: 'PATCH',
|
||||
body: { recaptcha_response: token },
|
||||
},
|
||||
});
|
||||
}, [ apiFetch, handleRefreshError, hash, id, setStatus, toast ]);
|
||||
setStatus?.('WAITING_FOR_RESPONSE');
|
||||
toastId.current = toast({
|
||||
title: 'Please wait',
|
||||
description: 'Refetching metadata request sent',
|
||||
icon: <Spinner size="sm" mr={ 2 }/>,
|
||||
status: 'warning',
|
||||
duration: null,
|
||||
isClosable: false,
|
||||
});
|
||||
timeoutId.current = window.setTimeout(handleRefreshError, 2 * MINUTE);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: getErrorMessage(error) || 'Unable to initialize metadata update',
|
||||
status: 'warning',
|
||||
});
|
||||
setStatus?.('ERROR');
|
||||
}
|
||||
|
||||
}, [ apiFetch, handleRefreshError, hash, id, recaptcha, setStatus, toast ]);
|
||||
|
||||
const handleModalClose = React.useCallback(() => {
|
||||
setStatus?.('INITIAL');
|
||||
}, [ setStatus ]);
|
||||
|
||||
const handleReCaptchaChange = React.useCallback((token: string | null) => {
|
||||
if (token) {
|
||||
initializeUpdate(token);
|
||||
}
|
||||
}, [ initializeUpdate ]);
|
||||
|
||||
const handleFormSubmit: React.FormEventHandler<HTMLFormElement> = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
const data = new FormData(event.target as HTMLFormElement);
|
||||
const token = data.get('recaptcha_token');
|
||||
typeof token === 'string' && initializeUpdate(token);
|
||||
}, [ initializeUpdate ]);
|
||||
|
||||
const handleSocketMessage: SocketMessage.TokenInstanceMetadataFetched['handler'] = React.useCallback((payload) => {
|
||||
if (String(payload.token_id) !== id) {
|
||||
return;
|
||||
@@ -141,6 +132,15 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
handler: handleSocketMessage,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (status !== 'MODAL_OPENED') {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(initializeUpdate, 100);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [ status, initializeUpdate ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
timeoutId.current && window.clearTimeout(timeoutId.current);
|
||||
@@ -161,25 +161,12 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Sending request</ModalHeader>
|
||||
<ModalCloseButton/>
|
||||
<ModalBody mb={ 0 } minH="78px">
|
||||
{ config.services.reCaptchaV3.siteKey ? (
|
||||
{ config.services.reCaptchaV2.siteKey ? (
|
||||
<>
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
|
||||
<Center h="80px">
|
||||
<Spinner size="lg"/>
|
||||
</Center>
|
||||
<GoogleReCaptcha
|
||||
onVerify={ handleReCaptchaChange }
|
||||
refreshReCaptcha
|
||||
/>
|
||||
</GoogleReCaptchaProvider>
|
||||
{ /* ONLY FOR TEST PURPOSES */ }
|
||||
<chakra.form noValidate onSubmit={ handleFormSubmit } display="none">
|
||||
<chakra.input
|
||||
name="recaptcha_token"
|
||||
placeholder="reCaptcha token"
|
||||
/>
|
||||
<chakra.button type="submit">Submit</chakra.button>
|
||||
</chakra.form>
|
||||
<Center h="80px">
|
||||
<Spinner size="lg"/>
|
||||
</Center>
|
||||
<ReCaptcha ref={ recaptcha.ref }/>
|
||||
</>
|
||||
) : (
|
||||
<Alert status="error">
|
||||
|
||||
@@ -15010,7 +15010,7 @@ prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
prop-types@^15.5.0, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -15252,6 +15252,14 @@ rc@^1.2.7:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-async-script@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21"
|
||||
integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==
|
||||
dependencies:
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
prop-types "^15.5.0"
|
||||
|
||||
react-clientside-effect@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
|
||||
@@ -15307,12 +15315,13 @@ react-focus-lock@^2.5.2, react-focus-lock@^2.9.4:
|
||||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
react-google-recaptcha[email protected]:
|
||||
version "1.10.1"
|
||||
resolved "https://registry.yarnpkg.com/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz#5b125bc0dec123206431860e8800e188fc735aff"
|
||||
integrity sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==
|
||||
react-google-recaptcha@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab"
|
||||
integrity sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==
|
||||
dependencies:
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
prop-types "^15.5.0"
|
||||
react-async-script "^1.2.0"
|
||||
|
||||
[email protected]:
|
||||
version "7.52.1"
|
||||
|
||||