mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
migrate to reCAPTCHA v3
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
|
||||
SENTRY_CSP_REPORT_URI=https://sentry.io
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_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
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import services from '../services';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'My account';
|
||||
|
||||
const config: Feature<{ isEnabled: true }> = (() => {
|
||||
const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => {
|
||||
if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV3.siteKey) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
recaptchaSiteKey: services.reCaptchaV3.siteKey,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true',
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import services from '../services';
|
||||
const title = 'Export data to CSV file';
|
||||
|
||||
const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
|
||||
if (services.reCaptcha.siteKey) {
|
||||
if (services.reCaptchaV3.siteKey) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
reCaptcha: {
|
||||
siteKey: services.reCaptcha.siteKey,
|
||||
siteKey: services.reCaptchaV3.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.reCaptcha.siteKey && addressMetadata.isEnabled && apiHost) {
|
||||
if (services.reCaptchaV3.siteKey && addressMetadata.isEnabled && apiHost) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getEnvValue } from './utils';
|
||||
|
||||
export default Object.freeze({
|
||||
reCaptcha: {
|
||||
siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'),
|
||||
reCaptchaV3: {
|
||||
siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_V3_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_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
|
||||
|
||||
@@ -52,5 +52,5 @@ 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_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
|
||||
|
||||
@@ -806,7 +806,7 @@ const schema = yup
|
||||
|
||||
// 6. External services envs
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY: yup.string(),
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
|
||||
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
|
||||
|
||||
@@ -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_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_RE_CAPTCHA_V3_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
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | - | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED |
|
||||
| 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 | Replaces by NEXT_PUBLIC_HOMEPAGE_STATS |
|
||||
| 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.35.0 | Replaced by NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY |
|
||||
|
||||
+2
-2
@@ -439,7 +439,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_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | See [below](ENVS.md#google-recaptcha) | true | - | `<your-secret>` | v1.35.0+ |
|
||||
|
||||
|
||||
|
||||
@@ -775,4 +775,4 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key | - | - | `<your-secret>` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY | `string` | Google reCAPTCHA v3 site key | - | - | `<your-secret>` | v1.35.0+ |
|
||||
|
||||
@@ -3,7 +3,7 @@ import type CspDev from 'csp-dev';
|
||||
import config from 'configs/app';
|
||||
|
||||
export function googleReCaptcha(): CspDev.DirectiveDescriptor {
|
||||
if (!config.services.reCaptcha.siteKey) {
|
||||
if (!config.services.reCaptchaV3.siteKey) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "18.2.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-google-recaptcha-v3": "1.10.1",
|
||||
"react-hook-form": "^7.33.1",
|
||||
"react-identicons": "^1.2.5",
|
||||
"react-intersection-observer": "^9.5.2",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mode } from '@chakra-ui/theme-tools';
|
||||
|
||||
import scrollbar from './foundations/scrollbar';
|
||||
import addressEntity from './globals/address-entity';
|
||||
import recaptcha from './globals/recaptcha';
|
||||
import getDefaultTransitionProps from './utils/getDefaultTransitionProps';
|
||||
|
||||
const global = (props: StyleFunctionProps) => ({
|
||||
@@ -25,6 +26,7 @@ const global = (props: StyleFunctionProps) => ({
|
||||
},
|
||||
...scrollbar(props),
|
||||
...addressEntity(props),
|
||||
...recaptcha(),
|
||||
});
|
||||
|
||||
export default global;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const styles = () => {
|
||||
return {
|
||||
'.grecaptcha-badge': {
|
||||
zIndex: 'toast',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default styles;
|
||||
@@ -1,11 +1,13 @@
|
||||
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';
|
||||
|
||||
import type { FormFields } from './types';
|
||||
import type { CsvExportParams } from 'types/client/address';
|
||||
|
||||
import config from 'configs/app';
|
||||
import buildUrl from 'lib/api/buildUrl';
|
||||
import type { ResourceName } from 'lib/api/resources';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
@@ -43,7 +45,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
|
||||
to_period: exportType !== 'holders' ? data.to : null,
|
||||
filter_type: filterType,
|
||||
filter_value: filterValue,
|
||||
recaptcha_response: data.reCaptcha,
|
||||
recaptcha_v3_response: data.reCaptcha,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
@@ -76,37 +78,41 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
|
||||
|
||||
}, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]);
|
||||
|
||||
const disabledFeatureMessage = (
|
||||
<Alert status="error">
|
||||
CSV export is not available at the moment since reCaptcha is not configured for this application.
|
||||
Please contact the service maintainer to make necessary changes in the service configuration.
|
||||
</Alert>
|
||||
);
|
||||
if (!config.services.reCaptchaV3.siteKey) {
|
||||
return (
|
||||
<Alert status="error">
|
||||
CSV export is not available at the moment since reCaptcha is not configured for this application.
|
||||
Please contact the service maintainer to make necessary changes in the service configuration.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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 }/> }
|
||||
<FormFieldReCaptcha disabledFeatureMessage={ disabledFeatureMessage }/>
|
||||
</Flex>
|
||||
<Button
|
||||
variant="solid"
|
||||
size="lg"
|
||||
type="submit"
|
||||
mt={ 8 }
|
||||
isLoading={ formState.isSubmitting }
|
||||
loadingText="Download"
|
||||
isDisabled={ !formState.isValid }
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
|
||||
<FormProvider { ...formApi }>
|
||||
<chakra.form
|
||||
noValidate
|
||||
onSubmit={ handleSubmit(onFormSubmit) }
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
||||
@@ -77,56 +78,61 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
|
||||
onSubmitResult(result);
|
||||
}, [ apiFetch, onSubmitResult ]);
|
||||
|
||||
if (!appConfig.services.reCaptchaV3.siteKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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)' }}
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ appConfig.services.reCaptchaV3.siteKey }>
|
||||
<FormProvider { ...formApi }>
|
||||
<chakra.form
|
||||
noValidate
|
||||
onSubmit={ formApi.handleSubmit(onFormSubmit) }
|
||||
>
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4">
|
||||
Company info
|
||||
</GridItem>
|
||||
<PublicTagsSubmitFieldRequesterName/>
|
||||
<PublicTagsSubmitFieldRequesterEmail/>
|
||||
{ !isMobile && <div/> }
|
||||
<PublicTagsSubmitFieldCompanyName/>
|
||||
<PublicTagsSubmitFieldCompanyWebsite/>
|
||||
{ !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 }}>
|
||||
<PublicTagsSubmitFieldDescription/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={{ base: 1, lg: 3 }}>
|
||||
<FormFieldReCaptcha/>
|
||||
</GridItem>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
size="lg"
|
||||
type="submit"
|
||||
mt={ 3 }
|
||||
isDisabled={ !formApi.formState.isValid }
|
||||
isLoading={ formApi.formState.isSubmitting }
|
||||
loadingText="Send request"
|
||||
w="min-content"
|
||||
<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>
|
||||
<PublicTagsSubmitFieldRequesterName/>
|
||||
<PublicTagsSubmitFieldRequesterEmail/>
|
||||
{ !isMobile && <div/> }
|
||||
<PublicTagsSubmitFieldCompanyName/>
|
||||
<PublicTagsSubmitFieldCompanyWebsite/>
|
||||
{ !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 }}>
|
||||
<PublicTagsSubmitFieldDescription/>
|
||||
</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>
|
||||
</Button>
|
||||
</Grid>
|
||||
</chakra.form>
|
||||
</FormProvider>
|
||||
</GoogleReCaptchaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Text } from '@chakra-ui/react';
|
||||
import { Button, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import ReCaptcha from 'react-google-recaptcha';
|
||||
import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
|
||||
import config from 'configs/app';
|
||||
import buildUrl from 'lib/api/buildUrl';
|
||||
@@ -13,58 +13,55 @@ 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 | null) => {
|
||||
const handleReCaptchaChange = React.useCallback(async(token: string) => {
|
||||
setToken(token);
|
||||
}, [ ]);
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const url = buildUrl('api_v2_key');
|
||||
const handleSubmit = React.useCallback(async() => {
|
||||
try {
|
||||
const url = buildUrl('api_v2_key');
|
||||
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
body: { recaptcha_response: token },
|
||||
credentials: 'include',
|
||||
}, {
|
||||
resource: 'api_v2_key',
|
||||
});
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
body: { recaptcha_v3_response: token },
|
||||
credentials: 'include',
|
||||
}, {
|
||||
resource: 'api_v2_key',
|
||||
});
|
||||
|
||||
window.location.reload();
|
||||
window.location.reload();
|
||||
|
||||
} catch (error) {
|
||||
toast({
|
||||
position: 'top-right',
|
||||
title: 'Error',
|
||||
description: 'Unable to get client key.',
|
||||
status: 'error',
|
||||
variant: 'subtle',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
position: 'top-right',
|
||||
title: 'Error',
|
||||
description: 'Unable to get client key.',
|
||||
status: 'error',
|
||||
variant: 'subtle',
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, [ toast, fetch ]);
|
||||
}, [ token, toast, fetch ]);
|
||||
|
||||
if (!config.services.reCaptchaV3.siteKey) {
|
||||
throw new Error('reCAPTCHA V3 site key is not set');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
'.recaptcha': {
|
||||
mt: 8,
|
||||
h: '78px', // otherwise content will jump after reCaptcha is loaded
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{ config.services.reCaptcha.siteKey && (
|
||||
<ReCaptcha
|
||||
className="recaptcha"
|
||||
sitekey={ config.services.reCaptcha.siteKey }
|
||||
onChange={ handleReCaptchaChange }
|
||||
/>
|
||||
) }
|
||||
</Box>
|
||||
<GoogleReCaptcha
|
||||
onVerify={ handleReCaptchaChange }
|
||||
refreshReCaptcha
|
||||
/>
|
||||
<Button onClick={ handleSubmit } mt={ 8 }>Try again</Button>
|
||||
</GoogleReCaptchaProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReCaptcha from 'react-google-recaptcha';
|
||||
import { GoogleReCaptcha } from 'react-google-recaptcha-v3';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import config from 'configs/app';
|
||||
const FormFieldReCaptcha = () => {
|
||||
|
||||
interface Props {
|
||||
disabledFeatureMessage?: JSX.Element;
|
||||
}
|
||||
|
||||
const FormFieldReCaptcha = ({ disabledFeatureMessage }: Props) => {
|
||||
|
||||
const { register, unregister, trigger, clearErrors, setValue, resetField, setError, formState } = useFormContext();
|
||||
const ref = React.useRef<ReCaptcha>(null);
|
||||
const { register, unregister, clearErrors, setValue, formState } = useFormContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
register('reCaptcha', { required: true, shouldUnregister: true });
|
||||
@@ -22,35 +15,15 @@ const FormFieldReCaptcha = ({ disabledFeatureMessage }: Props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
ref.current?.reset();
|
||||
trigger('reCaptcha');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ formState.submitCount ]);
|
||||
|
||||
const handleReCaptchaChange = React.useCallback((token: string | null) => {
|
||||
if (token) {
|
||||
clearErrors('reCaptcha');
|
||||
setValue('reCaptcha', token, { shouldValidate: true });
|
||||
}
|
||||
const handleReCaptchaChange = React.useCallback((token: string) => {
|
||||
clearErrors('reCaptcha');
|
||||
setValue('reCaptcha', token, { shouldValidate: true });
|
||||
}, [ clearErrors, setValue ]);
|
||||
|
||||
const handleReCaptchaExpire = React.useCallback(() => {
|
||||
resetField('reCaptcha');
|
||||
setError('reCaptcha', { type: 'required' });
|
||||
}, [ resetField, setError ]);
|
||||
|
||||
if (!config.services.reCaptcha.siteKey) {
|
||||
return disabledFeatureMessage ?? null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReCaptcha
|
||||
className="recaptcha"
|
||||
ref={ ref }
|
||||
sitekey={ config.services.reCaptcha.siteKey }
|
||||
onChange={ handleReCaptchaChange }
|
||||
onExpired={ handleReCaptchaExpire }
|
||||
<GoogleReCaptcha
|
||||
onVerify={ handleReCaptchaChange }
|
||||
refreshReCaptcha={ formState.submitCount ?? -1 }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
|
||||
import type { Screen, ScreenSuccess } from './types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
|
||||
import * as mixpanel from 'lib/mixpanel';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
@@ -16,6 +18,8 @@ import AuthModalScreenSuccessEmail from './screens/AuthModalScreenSuccessEmail';
|
||||
import AuthModalScreenSuccessWallet from './screens/AuthModalScreenSuccessWallet';
|
||||
import useProfileQuery from './useProfileQuery';
|
||||
|
||||
const feature = config.features.account;
|
||||
|
||||
interface Props {
|
||||
initialScreen: Screen;
|
||||
onClose: (isSuccess?: boolean) => void;
|
||||
@@ -154,6 +158,10 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
|
||||
}
|
||||
})();
|
||||
|
||||
if (!feature.isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen onClose={ onModalClose } size={{ base: 'full', lg: 'sm' }}>
|
||||
<ModalOverlay/>
|
||||
@@ -174,7 +182,9 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
|
||||
</ModalHeader>
|
||||
<ModalCloseButton top={ 6 } right={ 6 } color="gray.400"/>
|
||||
<ModalBody mb={ 0 }>
|
||||
{ content }
|
||||
<GoogleReCaptchaProvider reCaptchaKey={ feature.recaptchaSiteKey }>
|
||||
{ content }
|
||||
</GoogleReCaptchaProvider>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -9,7 +10,6 @@ import useApiFetch from 'lib/api/useApiFetch';
|
||||
import getErrorMessage from 'lib/errors/getErrorMessage';
|
||||
import useToast from 'lib/hooks/useToast';
|
||||
import * as mixpanel from 'lib/mixpanel';
|
||||
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
|
||||
|
||||
import AuthModalFieldEmail from '../fields/AuthModalFieldEmail';
|
||||
|
||||
@@ -27,6 +27,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
|
||||
const apiFetch = useApiFetch();
|
||||
const toast = useToast();
|
||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||
|
||||
const formApi = useForm<EmailFormFields>({
|
||||
mode: 'onBlur',
|
||||
@@ -35,13 +36,15 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const onFormSubmit: SubmitHandler<EmailFormFields> = React.useCallback((formData) => {
|
||||
const onFormSubmit: SubmitHandler<EmailFormFields> = React.useCallback(async(formData) => {
|
||||
const token = await executeRecaptcha?.();
|
||||
|
||||
return apiFetch('auth_send_otp', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email: formData.email,
|
||||
recaptcha_response: formData.reCaptcha,
|
||||
recaptcha_v3_response: token,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -67,7 +70,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
description: getErrorMessage(error) || 'Something went wrong',
|
||||
});
|
||||
});
|
||||
}, [ apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]);
|
||||
}, [ executeRecaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]);
|
||||
|
||||
return (
|
||||
<FormProvider { ...formApi }>
|
||||
@@ -76,8 +79,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
|
||||
onSubmit={ formApi.handleSubmit(onFormSubmit) }
|
||||
>
|
||||
<Text>Account email, used for transaction notifications from your watchlist.</Text>
|
||||
<AuthModalFieldEmail mt={ 6 } mb={ 3 }/>
|
||||
<FormFieldReCaptcha/>
|
||||
<AuthModalFieldEmail mt={ 6 }/>
|
||||
<Button
|
||||
mt={ 6 }
|
||||
type="submit"
|
||||
|
||||
@@ -27,7 +27,6 @@ export type Screen = {
|
||||
|
||||
export interface EmailFormFields {
|
||||
email: string;
|
||||
reCaptcha: string;
|
||||
}
|
||||
|
||||
export interface OtpCodeFormFields {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ToastId } from '@chakra-ui/react';
|
||||
import { chakra, Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner } from '@chakra-ui/react';
|
||||
import { chakra, Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner, Center } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import ReCaptcha from 'react-google-recaptcha';
|
||||
import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
|
||||
import type { SocketMessage } from 'lib/socket/types';
|
||||
import type { TokenInstance } from 'types/api/token';
|
||||
@@ -47,7 +47,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
pathParams: { hash, id },
|
||||
fetchParams: {
|
||||
method: 'PATCH',
|
||||
body: { recaptcha_response: reCaptchaToken },
|
||||
body: { recaptcha_v3_response: reCaptchaToken },
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
@@ -150,33 +150,43 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (status !== 'MODAL_OPENED') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={ status === 'MODAL_OPENED' } onClose={ handleModalClose } size={{ base: 'full', lg: 'sm' }}>
|
||||
<ModalOverlay/>
|
||||
<ModalContent>
|
||||
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Solve captcha to refresh metadata</ModalHeader>
|
||||
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Sending request</ModalHeader>
|
||||
<ModalCloseButton/>
|
||||
<ModalBody mb={ 0 } minH="78px">
|
||||
{ config.services.reCaptcha.siteKey ? (
|
||||
<ReCaptcha
|
||||
className="recaptcha"
|
||||
sitekey={ config.services.reCaptcha.siteKey }
|
||||
onChange={ handleReCaptchaChange }
|
||||
/>
|
||||
{ config.services.reCaptchaV3.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>
|
||||
</>
|
||||
) : (
|
||||
<Alert status="error">
|
||||
Metadata refresh is not available at the moment since reCaptcha is not configured for this application.
|
||||
Please contact the service maintainer to make necessary changes in the service configuration.
|
||||
</Alert>
|
||||
) }
|
||||
{ /* 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>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -13592,6 +13592,13 @@ react-focus-lock@^2.5.2, react-focus-lock@^2.9.4:
|
||||
use-callback-ref "^1.3.0"
|
||||
use-sidecar "^1.1.2"
|
||||
|
||||
[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==
|
||||
dependencies:
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user