Commit c9894eb8 authored by tom goriunov's avatar tom goriunov Committed by GitHub

ReCaptcha: allow the user to display the widget if an error occurs. (#2669)

Fixes #2653
parent 857b77de
...@@ -8,6 +8,7 @@ import dayjs from 'lib/date/dayjs'; ...@@ -8,6 +8,7 @@ import dayjs from 'lib/date/dayjs';
import downloadBlob from 'lib/downloadBlob'; import downloadBlob from 'lib/downloadBlob';
import { Button } from 'toolkit/chakra/button'; import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster'; import { toaster } from 'toolkit/chakra/toaster';
import { Tooltip } from 'toolkit/chakra/tooltip';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha'; import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha'; import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
...@@ -63,16 +64,22 @@ const ExportCSV = ({ filters }: Props) => { ...@@ -63,16 +64,22 @@ const ExportCSV = ({ filters }: Props) => {
return ( return (
<> <>
<Button <Tooltip
onClick={ handleExportCSV } content="This feature is not available due to a reCAPTCHA initialization error. Please contact the project team on Discord to report this issue."
variant="outline" disabled={ !recaptcha.isInitError }
loading={ isLoading }
size="sm"
mr={ 3 }
> >
Export to CSV <Button
</Button> onClick={ handleExportCSV }
<ReCaptcha ref={ recaptcha.ref }/> variant="outline"
loading={ isLoading }
size="sm"
mr={ 3 }
disabled={ recaptcha.isInitError }
>
Export to CSV
</Button>
</Tooltip>
<ReCaptcha { ...recaptcha } hideWarning/>
</> </>
); );
}; };
......
...@@ -101,14 +101,14 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla ...@@ -101,14 +101,14 @@ const CsvExportForm = ({ hash, resource, filterType, filterValue, fileNameTempla
{ exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> } { exportType !== 'holders' && <CsvExportFormField name="from" formApi={ formApi }/> }
{ exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> } { exportType !== 'holders' && <CsvExportFormField name="to" formApi={ formApi }/> }
</Flex> </Flex>
<ReCaptcha ref={ recaptcha.ref }/> <ReCaptcha { ...recaptcha }/>
<Button <Button
variant="solid" variant="solid"
type="submit" type="submit"
mt={ 8 } mt={ 8 }
loading={ formState.isSubmitting } loading={ formState.isSubmitting }
loadingText="Download" loadingText="Download"
disabled={ Boolean(formState.errors.from || formState.errors.to) } disabled={ Boolean(formState.errors.from || formState.errors.to || recaptcha.isInitError) }
> >
Download Download
</Button> </Button>
......
...@@ -89,14 +89,14 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -89,14 +89,14 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
isReadOnly={ !config.services.reCaptchaV2.siteKey || Boolean(profileQuery.data?.email) } isReadOnly={ !config.services.reCaptchaV2.siteKey || Boolean(profileQuery.data?.email) }
defaultValue={ profileQuery.data?.email || undefined } defaultValue={ profileQuery.data?.email || undefined }
/> />
{ config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && <ReCaptcha ref={ recaptcha.ref }/> } { config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && <ReCaptcha { ...recaptcha }/> }
{ config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && ( { config.services.reCaptchaV2.siteKey && !profileQuery.data?.email && (
<Button <Button
mt={ 6 } mt={ 6 }
size="sm" size="sm"
variant="outline" variant="outline"
type="submit" type="submit"
disabled={ formApi.formState.isSubmitting || !hasDirtyFields } disabled={ formApi.formState.isSubmitting || !hasDirtyFields || recaptcha.isInitError }
loading={ formApi.formState.isSubmitting } loading={ formApi.formState.isSubmitting }
loadingText="Save changes" loadingText="Save changes"
> >
......
...@@ -129,9 +129,10 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -129,9 +129,10 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
/> />
</GridItem> </GridItem>
<GridItem colSpan={{ base: 1, lg: 3 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<ReCaptcha ref={ recaptcha.ref }/> <ReCaptcha { ...recaptcha }/>
</GridItem> </GridItem>
{ !isMobile && <div/> }
<Button <Button
variant="solid" variant="solid"
...@@ -140,6 +141,7 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => { ...@@ -140,6 +141,7 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
loading={ formApi.formState.isSubmitting } loading={ formApi.formState.isSubmitting }
loadingText="Send request" loadingText="Send request"
w="min-content" w="min-content"
disabled={ recaptcha.isInitError }
> >
Send request Send request
</Button> </Button>
......
...@@ -51,8 +51,8 @@ const AppErrorTooManyRequests = () => { ...@@ -51,8 +51,8 @@ const AppErrorTooManyRequests = () => {
<Text color="text.secondary" mt={ 3 }> <Text color="text.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>
<ReCaptcha ref={ recaptcha.ref }/> <ReCaptcha { ...recaptcha }/>
<Button onClick={ handleSubmit } mt={ 8 }>Try again</Button> <Button onClick={ handleSubmit } disabled={ recaptcha.isInitError } mt={ 8 }>Try again</Button>
</> </>
); );
}; };
......
...@@ -2,30 +2,61 @@ import React from 'react'; ...@@ -2,30 +2,61 @@ import React from 'react';
import ReCaptcha from 'react-google-recaptcha'; import ReCaptcha from 'react-google-recaptcha';
import config from 'configs/app'; import config from 'configs/app';
import { Alert } from 'toolkit/chakra/alert';
import { Link } from 'toolkit/chakra/link';
interface Props { interface Props {
disabledFeatureMessage?: React.ReactNode; onInitError: () => void;
hideWarning?: boolean;
} }
const ReCaptchaInvisible = ({ disabledFeatureMessage }: Props, ref: React.Ref<ReCaptcha>) => { const ReCaptchaInvisible = ({ onInitError, hideWarning = false }: Props, ref: React.Ref<ReCaptcha>) => {
const [ attempt, setAttempt ] = React.useState(0); const [ attempt, setAttempt ] = React.useState(0);
const [ isError, setIsError ] = React.useState(false);
const [ , setIsVisible ] = React.useState(false);
const handleChange = React.useCallback(() => { const handleChange = React.useCallback(() => {
setAttempt(attempt + 1); setAttempt(attempt + 1);
}, [ attempt ]); }, [ attempt ]);
const handleError = React.useCallback(() => {
setIsError(true);
onInitError();
}, [ onInitError ]);
const handleClick = React.useCallback(() => {
const badge = window.document.querySelector('.grecaptcha-badge');
if (badge) {
setIsVisible((prev) => {
const nextValue = !prev;
(badge as HTMLElement).style.visibility = nextValue ? 'visible' : 'hidden';
(badge as HTMLElement).style.right = nextValue ? '14px' : '-1000px';
return nextValue;
});
}
}, [ ]);
if (!config.services.reCaptchaV2.siteKey) { if (!config.services.reCaptchaV2.siteKey) {
return disabledFeatureMessage ?? null; return null;
} }
return ( return (
<ReCaptcha <>
ref={ ref } <ReCaptcha
key={ attempt } ref={ ref }
sitekey={ config.services.reCaptchaV2.siteKey } key={ attempt }
size="invisible" sitekey={ config.services.reCaptchaV2.siteKey }
onChange={ handleChange } size="invisible"
/> onChange={ handleChange }
onErrored={ handleError }
/>
{ isError && !hideWarning && (
<Alert status="warning" whiteSpace="pre-wrap" w="fit-content" mt={ 3 } descriptionProps={{ display: 'block' }}>
This feature is not available due to a reCAPTCHA initialization error. Please contact the project team on Discord to report this issue.
Click <Link onClick={ handleClick } display="inline">here</Link> to show/hide reCAPTCHA widget content.
</Alert>
) }
</>
); );
}; };
......
...@@ -6,6 +6,7 @@ export default function useReCaptcha() { ...@@ -6,6 +6,7 @@ export default function useReCaptcha() {
const rejectCb = React.useRef<((error: Error) => void) | null>(null); const rejectCb = React.useRef<((error: Error) => void) | null>(null);
const [ isOpen, setIsOpen ] = React.useState(false); const [ isOpen, setIsOpen ] = React.useState(false);
const [ isInitError, setIsInitError ] = React.useState(false);
const executeAsync: () => Promise<string | null> = React.useCallback(async() => { const executeAsync: () => Promise<string | null> = React.useCallback(async() => {
setIsOpen(true); setIsOpen(true);
...@@ -22,6 +23,10 @@ export default function useReCaptcha() { ...@@ -22,6 +23,10 @@ export default function useReCaptcha() {
rejectCb.current?.(new Error('ReCaptcha is not solved')); rejectCb.current?.(new Error('ReCaptcha is not solved'));
}, []); }, []);
const handleInitError = React.useCallback(() => {
setIsInitError(true);
}, []);
React.useEffect(() => { React.useEffect(() => {
if (!isOpen) { if (!isOpen) {
return; return;
...@@ -35,5 +40,5 @@ export default function useReCaptcha() { ...@@ -35,5 +40,5 @@ export default function useReCaptcha() {
}; };
}, [ isOpen, handleContainerClick ]); }, [ isOpen, handleContainerClick ]);
return React.useMemo(() => ({ ref, executeAsync }), [ ref, executeAsync ]); return React.useMemo(() => ({ ref, executeAsync, isInitError, onInitError: handleInitError }), [ ref, executeAsync, isInitError, handleInitError ]);
} }
...@@ -47,9 +47,9 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source, logi ...@@ -47,9 +47,9 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source, logi
}, [ start ]); }, [ start ]);
return ( return (
<Center h="100px"> <Center minH="100px" flexDir="column">
<Spinner size="xl"/> { !recaptcha.isInitError && <Spinner size="xl"/> }
<ReCaptcha ref={ recaptcha.ref }/> <ReCaptcha { ...recaptcha }/>
</Center> </Center>
); );
}; };
......
...@@ -85,16 +85,16 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -85,16 +85,16 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
bgColor="dialog.bg" bgColor="dialog.bg"
mt={ 6 } mt={ 6 }
/> />
<ReCaptcha { ...recaptcha }/>
<Button <Button
mt={ 6 } mt={ 6 }
type="submit" type="submit"
disabled={ formApi.formState.isSubmitting } disabled={ formApi.formState.isSubmitting || recaptcha.isInitError }
loading={ formApi.formState.isSubmitting } loading={ formApi.formState.isSubmitting }
loadingText="Send a code" loadingText="Send a code"
> >
Send a code Send a code
</Button> </Button>
<ReCaptcha ref={ recaptcha.ref }/>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
); );
......
...@@ -108,22 +108,22 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -108,22 +108,22 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
and enter your code below. and enter your code below.
</Text> </Text>
<AuthModalFieldOtpCode isDisabled={ isCodeSending }/> <AuthModalFieldOtpCode isDisabled={ isCodeSending }/>
<ReCaptcha ref={ recaptcha.ref }/>
<Button <Button
variant="link" variant="link"
columnGap={ 2 } columnGap={ 2 }
mt={ 3 } mt={ 3 }
disabled={ isCodeSending } disabled={ isCodeSending || recaptcha.isInitError }
onClick={ handleResendCodeClick } onClick={ handleResendCodeClick }
> >
<IconSvg name="repeat" boxSize={ 5 }/> <IconSvg name="repeat" boxSize={ 5 }/>
<Box fontSize="sm">Resend code</Box> <Box fontSize="sm">Resend code</Box>
</Button> </Button>
<ReCaptcha { ...recaptcha }/>
<Button <Button
mt={ 6 } mt={ 6 }
type="submit" type="submit"
loading={ formApi.formState.isSubmitting } loading={ formApi.formState.isSubmitting }
disabled={ formApi.formState.isSubmitting || isCodeSending } disabled={ formApi.formState.isSubmitting || isCodeSending || recaptcha.isInitError }
loadingText="Submit" loadingText="Submit"
onClick={ formApi.handleSubmit(onFormSubmit) } onClick={ formApi.handleSubmit(onFormSubmit) }
> >
......
...@@ -170,7 +170,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -170,7 +170,7 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
<Center h="80px"> <Center h="80px">
<Spinner size="lg"/> <Spinner size="lg"/>
</Center> </Center>
<ReCaptcha ref={ recaptcha.ref }/> <ReCaptcha { ...recaptcha } hideWarning/>
</> </>
) : ( ) : (
<Alert status="error"> <Alert status="error">
......
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