Commit b883f4c8 authored by tom's avatar tom

migrate to reCAPTCHA v3

parent 2f496cf3
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
......
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,
......
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 |
\ No newline at end of file
| 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 |
......@@ -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+ |
&nbsp;
......@@ -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 {};
}
......
......@@ -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;
const styles = () => {
return {
'.grecaptcha-badge': {
zIndex: 'toast',
},
};
};
export default styles;
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>
);
};
......
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">
<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 }}>
</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"
>
<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>
);
};
......
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>
);
};
......
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 }
/>
);
};
......
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>
......
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 {
......
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"
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:
version "3.1.0"
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