Commit b883f4c8 authored by tom's avatar tom

migrate to reCAPTCHA v3

parent 2f496cf3
NEXT_PUBLIC_SENTRY_DSN=https://sentry.io NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
SENTRY_CSP_REPORT_URI=https://sentry.io SENTRY_CSP_REPORT_URI=https://sentry.io
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx 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_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
......
import type { Feature } from './types'; import type { Feature } from './types';
import services from '../services';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const title = 'My account'; 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({ return Object.freeze({
title, title,
isEnabled: getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true', isEnabled: false,
}); });
})(); })();
......
...@@ -5,12 +5,12 @@ import services from '../services'; ...@@ -5,12 +5,12 @@ import services from '../services';
const title = 'Export data to CSV file'; const title = 'Export data to CSV file';
const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => { const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
if (services.reCaptcha.siteKey) { if (services.reCaptchaV3.siteKey) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
reCaptcha: { reCaptcha: {
siteKey: services.reCaptcha.siteKey, siteKey: services.reCaptchaV3.siteKey,
}, },
}); });
} }
......
...@@ -9,7 +9,7 @@ const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST'); ...@@ -9,7 +9,7 @@ const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Public tag submission'; const title = 'Public tag submission';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => { 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({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
......
import { getEnvValue } from './utils'; import { getEnvValue } from './utils';
export default Object.freeze({ export default Object.freeze({
reCaptcha: { reCaptchaV3: {
siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY'), siteKey: getEnvValue('NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY'),
}, },
}); });
...@@ -49,4 +49,4 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx ...@@ -49,4 +49,4 @@ NEXT_PUBLIC_AUTH0_CLIENT_ID=xxx
NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004 NEXT_PUBLIC_STATS_API_HOST=https://localhost:3004
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005 NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://localhost:3006 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 ...@@ -52,5 +52,5 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006 NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007 NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008 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 NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
...@@ -806,7 +806,7 @@ const schema = yup ...@@ -806,7 +806,7 @@ const schema = yup
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), 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_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(), NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(), NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
......
...@@ -3,7 +3,7 @@ NEXT_PUBLIC_AUTH_URL=https://example.com ...@@ -3,7 +3,7 @@ NEXT_PUBLIC_AUTH_URL=https://example.com
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_LOGOUT_URL=https://example.com NEXT_PUBLIC_LOGOUT_URL=https://example.com
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx 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_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
......
...@@ -10,4 +10,5 @@ ...@@ -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_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_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_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 |
\ No newline at end of file | 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 |
...@@ -439,7 +439,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -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 | | 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+ |
&nbsp; &nbsp;
...@@ -775,4 +775,4 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt ...@@ -775,4 +775,4 @@ For obtaining the variables values please refer to [reCAPTCHA documentation](htt
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | 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'; ...@@ -3,7 +3,7 @@ import type CspDev from 'csp-dev';
import config from 'configs/app'; import config from 'configs/app';
export function googleReCaptcha(): CspDev.DirectiveDescriptor { export function googleReCaptcha(): CspDev.DirectiveDescriptor {
if (!config.services.reCaptcha.siteKey) { if (!config.services.reCaptchaV3.siteKey) {
return {}; return {};
} }
......
...@@ -3,6 +3,7 @@ import { mode } from '@chakra-ui/theme-tools'; ...@@ -3,6 +3,7 @@ import { mode } from '@chakra-ui/theme-tools';
import scrollbar from './foundations/scrollbar'; import scrollbar from './foundations/scrollbar';
import addressEntity from './globals/address-entity'; import addressEntity from './globals/address-entity';
import recaptcha from './globals/recaptcha';
import getDefaultTransitionProps from './utils/getDefaultTransitionProps'; import getDefaultTransitionProps from './utils/getDefaultTransitionProps';
const global = (props: StyleFunctionProps) => ({ const global = (props: StyleFunctionProps) => ({
...@@ -25,6 +26,7 @@ const global = (props: StyleFunctionProps) => ({ ...@@ -25,6 +26,7 @@ const global = (props: StyleFunctionProps) => ({
}, },
...scrollbar(props), ...scrollbar(props),
...addressEntity(props), ...addressEntity(props),
...recaptcha(),
}); });
export default global; export default global;
const styles = () => {
return {
'.grecaptcha-badge': {
zIndex: 'toast',
},
};
};
export default styles;
import { Alert, Button, chakra, Flex } from '@chakra-ui/react'; import { Alert, Button, chakra, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import type { FormFields } from './types'; import type { FormFields } from './types';
import type { CsvExportParams } from 'types/client/address'; import type { CsvExportParams } from 'types/client/address';
import config from 'configs/app';
import buildUrl from 'lib/api/buildUrl'; import buildUrl from 'lib/api/buildUrl';
import type { ResourceName } from 'lib/api/resources'; import type { ResourceName } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
...@@ -43,7 +45,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -43,7 +45,7 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
to_period: exportType !== 'holders' ? data.to : null, to_period: exportType !== 'holders' ? data.to : null,
filter_type: filterType, filter_type: filterType,
filter_value: filterValue, filter_value: filterValue,
recaptcha_response: data.reCaptcha, recaptcha_v3_response: data.reCaptcha,
}); });
const response = await fetch(url, { const response = await fetch(url, {
...@@ -76,37 +78,41 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -76,37 +78,41 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
}, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]); }, [ resource, hash, exportType, filterType, filterValue, fileNameTemplate, toast ]);
const disabledFeatureMessage = ( if (!config.services.reCaptchaV3.siteKey) {
<Alert status="error"> return (
CSV export is not available at the moment since reCaptcha is not configured for this application. <Alert status="error">
Please contact the service maintainer to make necessary changes in the service configuration. CSV export is not available at the moment since reCaptcha is not configured for this application.
</Alert> Please contact the service maintainer to make necessary changes in the service configuration.
); </Alert>
);
}
return ( return (
<FormProvider { ...formApi }> <GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
<chakra.form <FormProvider { ...formApi }>
noValidate <chakra.form
onSubmit={ handleSubmit(onFormSubmit) } 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 }
> >
Download <Flex columnGap={ 5 } rowGap={ 3 } flexDir={{ base: 'column', lg: 'row' }} alignItems={{ base: 'flex-start', lg: 'center' }} flexWrap="wrap">
</Button> { exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> }
</chakra.form> { exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> }
</FormProvider> <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>
); );
}; };
......
import { Button, chakra, Grid, GridItem } from '@chakra-ui/react'; import { Button, chakra, Grid, GridItem } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
...@@ -77,56 +78,61 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -77,56 +78,61 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
onSubmitResult(result); onSubmitResult(result);
}, [ apiFetch, onSubmitResult ]); }, [ apiFetch, onSubmitResult ]);
if (!appConfig.services.reCaptchaV3.siteKey) {
return null;
}
return ( return (
<FormProvider { ...formApi }> <GoogleReCaptchaProvider reCaptchaKey={ appConfig.services.reCaptchaV3.siteKey }>
<chakra.form <FormProvider { ...formApi }>
noValidate <chakra.form
onSubmit={ formApi.handleSubmit(onFormSubmit) } 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)' }}
> >
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4"> <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 Company info
</GridItem> </GridItem>
<PublicTagsSubmitFieldRequesterName/> <PublicTagsSubmitFieldRequesterName/>
<PublicTagsSubmitFieldRequesterEmail/> <PublicTagsSubmitFieldRequesterEmail/>
{ !isMobile && <div/> } { !isMobile && <div/> }
<PublicTagsSubmitFieldCompanyName/> <PublicTagsSubmitFieldCompanyName/>
<PublicTagsSubmitFieldCompanyWebsite/> <PublicTagsSubmitFieldCompanyWebsite/>
{ !isMobile && <div/> } { !isMobile && <div/> }
<GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}> <GridItem colSpan={{ base: 1, lg: 3 }} as="h2" textStyle="h4" mt={{ base: 3, lg: 5 }}>
Public tags/labels Public tags/labels
<Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link"/> <Hint label="Submit a public tag proposal for our moderation team to review" ml={ 1 } color="link"/>
</GridItem> </GridItem>
<PublicTagsSubmitFieldAddresses/> <PublicTagsSubmitFieldAddresses/>
<PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/> <PublicTagsSubmitFieldTags tagTypes={ config?.tagTypes }/>
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<PublicTagsSubmitFieldDescription/> <PublicTagsSubmitFieldDescription/>
</GridItem> </GridItem>
<GridItem colSpan={{ base: 1, lg: 3 }}> <GridItem colSpan={{ base: 1, lg: 3 }}>
<FormFieldReCaptcha/> <FormFieldReCaptcha/>
</GridItem> </GridItem>
<Button <Button
variant="solid" variant="solid"
size="lg" size="lg"
type="submit" type="submit"
mt={ 3 } mt={ 3 }
isDisabled={ !formApi.formState.isValid } isLoading={ formApi.formState.isSubmitting }
isLoading={ formApi.formState.isSubmitting } loadingText="Send request"
loadingText="Send request" w="min-content"
w="min-content" >
>
Send request Send request
</Button> </Button>
</Grid> </Grid>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
</GoogleReCaptchaProvider>
); );
}; };
......
import { Box, Text } from '@chakra-ui/react'; import { Button, Text } from '@chakra-ui/react';
import React from '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 config from 'configs/app';
import buildUrl from 'lib/api/buildUrl'; import buildUrl from 'lib/api/buildUrl';
...@@ -13,58 +13,55 @@ import AppErrorTitle from '../AppErrorTitle'; ...@@ -13,58 +13,55 @@ import AppErrorTitle from '../AppErrorTitle';
const AppErrorTooManyRequests = () => { const AppErrorTooManyRequests = () => {
const toast = useToast(); const toast = useToast();
const fetch = useFetch(); 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) { const handleSubmit = React.useCallback(async() => {
try { try {
const url = buildUrl('api_v2_key'); const url = buildUrl('api_v2_key');
await fetch(url, { await fetch(url, {
method: 'POST', method: 'POST',
body: { recaptcha_response: token }, body: { recaptcha_v3_response: token },
credentials: 'include', credentials: 'include',
}, { }, {
resource: 'api_v2_key', resource: 'api_v2_key',
}); });
window.location.reload(); window.location.reload();
} catch (error) { } catch (error) {
toast({ toast({
position: 'top-right', position: 'top-right',
title: 'Error', title: 'Error',
description: 'Unable to get client key.', description: 'Unable to get client key.',
status: 'error', status: 'error',
variant: 'subtle', variant: 'subtle',
isClosable: true, isClosable: true,
}); });
}
} }
}, [ toast, fetch ]); }, [ token, toast, fetch ]);
if (!config.services.reCaptchaV3.siteKey) {
throw new Error('reCAPTCHA V3 site key is not set');
}
return ( return (
<Box <GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
sx={{
'.recaptcha': {
mt: 8,
h: '78px', // otherwise content will jump after reCaptcha is loaded
},
}}
>
<AppErrorIcon statusCode={ 429 }/> <AppErrorIcon statusCode={ 429 }/>
<AppErrorTitle title="Too many requests"/> <AppErrorTitle title="Too many requests"/>
<Text variant="secondary" mt={ 3 }> <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. You have exceeded the request rate for a given time period. Please reduce the number of requests and try again soon.
</Text> </Text>
{ config.services.reCaptcha.siteKey && ( <GoogleReCaptcha
<ReCaptcha onVerify={ handleReCaptchaChange }
className="recaptcha" refreshReCaptcha
sitekey={ config.services.reCaptcha.siteKey } />
onChange={ handleReCaptchaChange } <Button onClick={ handleSubmit } mt={ 8 }>Try again</Button>
/> </GoogleReCaptchaProvider>
) }
</Box>
); );
}; };
......
import React from 'react'; import React from 'react';
import ReCaptcha from 'react-google-recaptcha'; import { GoogleReCaptcha } from 'react-google-recaptcha-v3';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import config from 'configs/app'; const FormFieldReCaptcha = () => {
interface Props { const { register, unregister, clearErrors, setValue, formState } = useFormContext();
disabledFeatureMessage?: JSX.Element;
}
const FormFieldReCaptcha = ({ disabledFeatureMessage }: Props) => {
const { register, unregister, trigger, clearErrors, setValue, resetField, setError, formState } = useFormContext();
const ref = React.useRef<ReCaptcha>(null);
React.useEffect(() => { React.useEffect(() => {
register('reCaptcha', { required: true, shouldUnregister: true }); register('reCaptcha', { required: true, shouldUnregister: true });
...@@ -22,35 +15,15 @@ const FormFieldReCaptcha = ({ disabledFeatureMessage }: Props) => { ...@@ -22,35 +15,15 @@ const FormFieldReCaptcha = ({ disabledFeatureMessage }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
React.useEffect(() => { const handleReCaptchaChange = React.useCallback((token: string) => {
ref.current?.reset(); clearErrors('reCaptcha');
trigger('reCaptcha'); setValue('reCaptcha', token, { shouldValidate: true });
// 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 });
}
}, [ clearErrors, setValue ]); }, [ clearErrors, setValue ]);
const handleReCaptchaExpire = React.useCallback(() => {
resetField('reCaptcha');
setError('reCaptcha', { type: 'required' });
}, [ resetField, setError ]);
if (!config.services.reCaptcha.siteKey) {
return disabledFeatureMessage ?? null;
}
return ( return (
<ReCaptcha <GoogleReCaptcha
className="recaptcha" onVerify={ handleReCaptchaChange }
ref={ ref } refreshReCaptcha={ formState.submitCount ?? -1 }
sitekey={ config.services.reCaptcha.siteKey }
onChange={ handleReCaptchaChange }
onExpired={ handleReCaptchaExpire }
/> />
); );
}; };
......
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react'; import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
import type { Screen, ScreenSuccess } from './types'; import type { Screen, ScreenSuccess } from './types';
import config from 'configs/app';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -16,6 +18,8 @@ import AuthModalScreenSuccessEmail from './screens/AuthModalScreenSuccessEmail'; ...@@ -16,6 +18,8 @@ import AuthModalScreenSuccessEmail from './screens/AuthModalScreenSuccessEmail';
import AuthModalScreenSuccessWallet from './screens/AuthModalScreenSuccessWallet'; import AuthModalScreenSuccessWallet from './screens/AuthModalScreenSuccessWallet';
import useProfileQuery from './useProfileQuery'; import useProfileQuery from './useProfileQuery';
const feature = config.features.account;
interface Props { interface Props {
initialScreen: Screen; initialScreen: Screen;
onClose: (isSuccess?: boolean) => void; onClose: (isSuccess?: boolean) => void;
...@@ -154,6 +158,10 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => { ...@@ -154,6 +158,10 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
} }
})(); })();
if (!feature.isEnabled) {
return null;
}
return ( return (
<Modal isOpen onClose={ onModalClose } size={{ base: 'full', lg: 'sm' }}> <Modal isOpen onClose={ onModalClose } size={{ base: 'full', lg: 'sm' }}>
<ModalOverlay/> <ModalOverlay/>
...@@ -174,7 +182,9 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => { ...@@ -174,7 +182,9 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
</ModalHeader> </ModalHeader>
<ModalCloseButton top={ 6 } right={ 6 } color="gray.400"/> <ModalCloseButton top={ 6 } right={ 6 } color="gray.400"/>
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
{ content } <GoogleReCaptchaProvider reCaptchaKey={ feature.recaptchaSiteKey }>
{ content }
</GoogleReCaptchaProvider>
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>
......
import { chakra, Button, Text } from '@chakra-ui/react'; import { chakra, Button, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useGoogleReCaptcha } from 'react-google-recaptcha-v3';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
...@@ -9,7 +10,6 @@ import useApiFetch from 'lib/api/useApiFetch'; ...@@ -9,7 +10,6 @@ import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/errors/getErrorMessage'; import getErrorMessage from 'lib/errors/getErrorMessage';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import AuthModalFieldEmail from '../fields/AuthModalFieldEmail'; import AuthModalFieldEmail from '../fields/AuthModalFieldEmail';
...@@ -27,6 +27,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -27,6 +27,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const { executeRecaptcha } = useGoogleReCaptcha();
const formApi = useForm<EmailFormFields>({ const formApi = useForm<EmailFormFields>({
mode: 'onBlur', mode: 'onBlur',
...@@ -35,13 +36,15 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -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', { return apiFetch('auth_send_otp', {
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { body: {
email: formData.email, email: formData.email,
recaptcha_response: formData.reCaptcha, recaptcha_v3_response: token,
}, },
}, },
}) })
...@@ -67,7 +70,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -67,7 +70,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
description: getErrorMessage(error) || 'Something went wrong', 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 ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -76,8 +79,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -76,8 +79,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
onSubmit={ formApi.handleSubmit(onFormSubmit) } onSubmit={ formApi.handleSubmit(onFormSubmit) }
> >
<Text>Account email, used for transaction notifications from your watchlist.</Text> <Text>Account email, used for transaction notifications from your watchlist.</Text>
<AuthModalFieldEmail mt={ 6 } mb={ 3 }/> <AuthModalFieldEmail mt={ 6 }/>
<FormFieldReCaptcha/>
<Button <Button
mt={ 6 } mt={ 6 }
type="submit" type="submit"
......
...@@ -27,7 +27,6 @@ export type Screen = { ...@@ -27,7 +27,6 @@ export type Screen = {
export interface EmailFormFields { export interface EmailFormFields {
email: string; email: string;
reCaptcha: string;
} }
export interface OtpCodeFormFields { export interface OtpCodeFormFields {
......
import type { ToastId } from '@chakra-ui/react'; 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 { useQueryClient } from '@tanstack/react-query';
import React from 'react'; 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 { SocketMessage } from 'lib/socket/types';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
...@@ -47,7 +47,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -47,7 +47,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
pathParams: { hash, id }, pathParams: { hash, id },
fetchParams: { fetchParams: {
method: 'PATCH', method: 'PATCH',
body: { recaptcha_response: reCaptchaToken }, body: { recaptcha_v3_response: reCaptchaToken },
}, },
}) })
.then(() => { .then(() => {
...@@ -150,33 +150,43 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -150,33 +150,43 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
if (status !== 'MODAL_OPENED') {
return null;
}
return ( return (
<Modal isOpen={ status === 'MODAL_OPENED' } onClose={ handleModalClose } size={{ base: 'full', lg: 'sm' }}> <Modal isOpen={ status === 'MODAL_OPENED' } onClose={ handleModalClose } size={{ base: 'full', lg: 'sm' }}>
<ModalOverlay/> <ModalOverlay/>
<ModalContent> <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/> <ModalCloseButton/>
<ModalBody mb={ 0 } minH="78px"> <ModalBody mb={ 0 } minH="78px">
{ config.services.reCaptcha.siteKey ? ( { config.services.reCaptchaV3.siteKey ? (
<ReCaptcha <>
className="recaptcha" <GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
sitekey={ config.services.reCaptcha.siteKey } <Center h="80px">
onChange={ handleReCaptchaChange } <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"> <Alert status="error">
Metadata refresh is not available at the moment since reCaptcha is not configured for this application. 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. Please contact the service maintainer to make necessary changes in the service configuration.
</Alert> </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> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>
......
...@@ -13592,6 +13592,13 @@ react-focus-lock@^2.5.2, react-focus-lock@^2.9.4: ...@@ -13592,6 +13592,13 @@ react-focus-lock@^2.5.2, react-focus-lock@^2.9.4:
use-callback-ref "^1.3.0" use-callback-ref "^1.3.0"
use-sidecar "^1.1.2" use-sidecar "^1.1.2"
react-google-recaptcha-v3@1.10.1:
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: react-google-recaptcha@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab" resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment