Commit d5cf7898 authored by tom's avatar tom

resend code and change email from profile page

parent b883f4c8
import { Button, chakra, FormControl, Heading, Input, InputGroup, InputRightElement, Text } from '@chakra-ui/react'; import { Button, chakra, Heading, useDisclosure } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
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 { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import type { FormFields } from './types';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import { EMAIL_REGEXP } from 'lib/validations/email'; import config from 'configs/app';
import IconSvg from 'ui/shared/IconSvg'; import useApiFetch from 'lib/api/useApiFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel';
import FormFieldReCaptcha from 'ui/shared/forms/fields/FormFieldReCaptcha';
import AuthModal from 'ui/snippets/auth/AuthModal';
interface FormFields { import MyProfileFieldsEmail from './fields/MyProfileFieldsEmail';
email: string;
} const MIXPANEL_CONFIG = {
account_link_info: {
source: 'Profile' as const,
},
};
interface Props { interface Props {
profileQuery: UseQueryResult<UserInfo, unknown>; profileQuery: UseQueryResult<UserInfo, unknown>;
} }
const MyProfileEmail = ({ profileQuery }: Props) => { const MyProfileEmail = ({ profileQuery }: Props) => {
const authModal = useDisclosure();
const apiFetch = useApiFetch();
const toast = useToast();
const formApi = useForm<FormFields>({ const formApi = useForm<FormFields>({
mode: 'onBlur', mode: 'onBlur',
defaultValues: { defaultValues: {
...@@ -26,12 +41,32 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -26,12 +41,32 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
}, },
}); });
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback((formData) => { const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(formData) => {
// eslint-disable-next-line no-console try {
console.log(formData); await apiFetch('auth_send_otp', {
}, [ ]); fetchParams: {
method: 'POST',
const isDisabled = formApi.formState.isSubmitting; body: {
email: formData.email,
recaptcha_v3_response: formData.reCaptcha,
},
},
});
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
Source: 'Profile',
Status: 'OTP sent',
Type: 'Email',
});
authModal.onOpen();
} catch (error) {
const apiError = getErrorObjPayload<{ message: string }>(error);
toast({
status: 'error',
title: 'Error',
description: apiError?.message || getErrorMessage(error) || 'Something went wrong',
});
}
}, [ apiFetch, authModal, toast ]);
return ( return (
<section> <section>
...@@ -41,37 +76,34 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -41,37 +76,34 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
noValidate noValidate
onSubmit={ formApi.handleSubmit(onFormSubmit) } onSubmit={ formApi.handleSubmit(onFormSubmit) }
> >
<FormControl variant="floating" isDisabled={ isDisabled } isRequired size="md"> <MyProfileFieldsEmail isReadOnly={ !config.services.reCaptchaV3.siteKey }/>
<InputGroup> { config.services.reCaptchaV3.siteKey && (
<Input <GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
{ ...formApi.register('email', { required: true, pattern: EMAIL_REGEXP }) } <FormFieldReCaptcha/>
required </GoogleReCaptchaProvider>
isInvalid={ Boolean(formApi.formState.errors.email) } ) }
isDisabled={ isDisabled } { config.services.reCaptchaV3.siteKey && (
autoComplete="off" <Button
/> mt={ 6 }
<InputPlaceholder text="Email" error={ formApi.formState.errors.email }/> size="sm"
{ !formApi.formState.isDirty && ( variant="outline"
<InputRightElement h="100%"> type="submit"
<IconSvg name="certified" boxSize={ 5 } color="green.500"/> isDisabled={ formApi.formState.isSubmitting || !formApi.formState.isDirty }
</InputRightElement> isLoading={ formApi.formState.isSubmitting }
) } loadingText="Save changes"
</InputGroup> >
<Text variant="secondary" mt={ 1 } fontSize="sm">Email for watch list notifications and private tags</Text> Save changes
</FormControl> </Button>
<Button ) }
mt={ 6 }
size="sm"
variant="outline"
type="submit"
isDisabled={ formApi.formState.isSubmitting || !formApi.formState.isDirty }
isLoading={ formApi.formState.isSubmitting }
loadingText="Save changes"
>
Save changes
</Button>
</chakra.form> </chakra.form>
</FormProvider> </FormProvider>
{ authModal.isOpen && (
<AuthModal
initialScreen={{ type: 'otp_code', isAuth: true, email: formApi.getValues('email') }}
onClose={ authModal.onClose }
mixpanelConfig={ MIXPANEL_CONFIG }
/>
) }
</section> </section>
); );
}; };
......
import { FormControl, Input, InputGroup, InputRightElement, Text } from '@chakra-ui/react';
import React from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type { FormFields } from '../types';
import { EMAIL_REGEXP } from 'lib/validations/email';
import IconSvg from 'ui/shared/IconSvg';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props {
isReadOnly?: boolean;
}
const MyProfileFieldsEmail = ({ isReadOnly }: Props) => {
const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, 'email'>({
control,
name: 'email',
rules: { required: true, pattern: EMAIL_REGEXP },
});
const isDisabled = formState.isSubmitting;
return (
<FormControl variant="floating" isDisabled={ isDisabled } isRequired size="md">
<InputGroup>
<Input
{ ...field }
required
isInvalid={ Boolean(fieldState.error) }
isDisabled={ isDisabled }
isReadOnly={ isReadOnly }
autoComplete="off"
bgColor="dialog_bg"
/>
<InputPlaceholder text="Email" error={ fieldState.error }/>
{ !formState.isDirty && (
<InputRightElement h="100%">
<IconSvg name="certified" boxSize={ 5 } color="green.500"/>
</InputRightElement>
) }
</InputGroup>
<Text variant="secondary" mt={ 1 } fontSize="sm">Email for watch list notifications and private tags</Text>
</FormControl>
);
};
export default React.memo(MyProfileFieldsEmail);
export interface FormFields {
email: string;
reCaptcha: string;
}
...@@ -24,7 +24,7 @@ interface Props { ...@@ -24,7 +24,7 @@ interface Props {
initialScreen: Screen; initialScreen: Screen;
onClose: (isSuccess?: boolean) => void; onClose: (isSuccess?: boolean) => void;
mixpanelConfig?: { mixpanelConfig?: {
'wallet_connect': { 'wallet_connect'?: {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source']; source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
}; };
'account_link_info': { 'account_link_info': {
...@@ -124,7 +124,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => { ...@@ -124,7 +124,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig }: Props) => {
onSuccess={ onAuthSuccess } onSuccess={ onAuthSuccess }
onError={ onReset } onError={ onReset }
isAuth={ currentStep.isAuth } isAuth={ currentStep.isAuth }
source={ mixpanelConfig?.wallet_connect.source } source={ mixpanelConfig?.wallet_connect?.source }
/> />
); );
case 'email': case 'email':
......
...@@ -8,7 +8,11 @@ import PinInput from 'ui/shared/chakra/PinInput'; ...@@ -8,7 +8,11 @@ import PinInput from 'ui/shared/chakra/PinInput';
const CODE_LENGTH = 6; const CODE_LENGTH = 6;
const AuthModalFieldOtpCode = () => { interface Props {
isDisabled?: boolean;
}
const AuthModalFieldOtpCode = ({ isDisabled: isDisabledProp }: Props) => {
const { control } = useFormContext<OtpCodeFormFields>(); const { control } = useFormContext<OtpCodeFormFields>();
const { field, fieldState, formState } = useController<OtpCodeFormFields, 'code'>({ const { field, fieldState, formState } = useController<OtpCodeFormFields, 'code'>({
control, control,
...@@ -16,7 +20,7 @@ const AuthModalFieldOtpCode = () => { ...@@ -16,7 +20,7 @@ const AuthModalFieldOtpCode = () => {
rules: { required: true, minLength: CODE_LENGTH, maxLength: CODE_LENGTH }, rules: { required: true, minLength: CODE_LENGTH, maxLength: CODE_LENGTH },
}); });
const isDisabled = formState.isSubmitting; const isDisabled = isDisabledProp || formState.isSubmitting;
return ( return (
<> <>
......
...@@ -37,39 +37,37 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -37,39 +37,37 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
}); });
const onFormSubmit: SubmitHandler<EmailFormFields> = React.useCallback(async(formData) => { const onFormSubmit: SubmitHandler<EmailFormFields> = React.useCallback(async(formData) => {
const token = await executeRecaptcha?.(); try {
const token = await executeRecaptcha?.();
return apiFetch('auth_send_otp', { await apiFetch('auth_send_otp', {
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { body: {
email: formData.email, email: formData.email,
recaptcha_v3_response: token, recaptcha_v3_response: token,
},
}, },
}, });
}) if (isAuth) {
.then(() => { mixpanelConfig?.account_link_info.source !== 'Profile' && mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, {
if (isAuth) { Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown',
mixpanel.logEvent(mixpanel.EventTypes.ACCOUNT_LINK_INFO, { Status: 'OTP sent',
Source: mixpanelConfig?.account_link_info.source ?? 'Profile dropdown', Type: 'Email',
Status: 'OTP sent', });
Type: 'Email', } else {
}); mixpanel.logEvent(mixpanel.EventTypes.LOGIN, {
} else { Action: 'OTP sent',
mixpanel.logEvent(mixpanel.EventTypes.LOGIN, { Source: 'Email',
Action: 'OTP sent',
Source: 'Email',
});
}
onSubmit({ type: 'otp_code', email: formData.email, isAuth });
})
.catch((error) => {
toast({
status: 'error',
title: 'Error',
description: getErrorMessage(error) || 'Something went wrong',
}); });
}
onSubmit({ type: 'otp_code', email: formData.email, isAuth });
} catch (error) {
toast({
status: 'error',
title: 'Error',
description: getErrorMessage(error) || 'Something went wrong',
}); });
}
}, [ executeRecaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]); }, [ executeRecaptcha, apiFetch, isAuth, onSubmit, mixpanelConfig?.account_link_info.source, toast ]);
return ( return (
...@@ -83,7 +81,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => { ...@@ -83,7 +81,7 @@ const AuthModalScreenEmail = ({ onSubmit, isAuth, mixpanelConfig }: Props) => {
<Button <Button
mt={ 6 } mt={ 6 }
type="submit" type="submit"
isDisabled={ formApi.formState.isSubmitting || !formApi.formState.isValid } isDisabled={ formApi.formState.isSubmitting }
isLoading={ formApi.formState.isSubmitting } isLoading={ formApi.formState.isSubmitting }
loadingText="Send a code" loadingText="Send a code"
> >
......
import { chakra, Box, Text, Button, Link } from '@chakra-ui/react'; import { chakra, Box, Text, Button } 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';
...@@ -23,6 +24,8 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -23,6 +24,8 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const { executeRecaptcha } = useGoogleReCaptcha();
const [ isCodeSending, setIsCodeSending ] = React.useState(false);
const formApi = useForm<OtpCodeFormFields>({ const formApi = useForm<OtpCodeFormFields>({
mode: 'onBlur', mode: 'onBlur',
...@@ -63,10 +66,12 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -63,10 +66,12 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
const handleResendCodeClick = React.useCallback(async() => { const handleResendCodeClick = React.useCallback(async() => {
try { try {
formApi.clearErrors('code'); formApi.clearErrors('code');
setIsCodeSending(true);
const token = await executeRecaptcha?.();
await apiFetch('auth_send_otp', { await apiFetch('auth_send_otp', {
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { email }, body: { email, recaptcha_v3_response: token },
}, },
}); });
...@@ -84,8 +89,10 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -84,8 +89,10 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
title: 'Error', title: 'Error',
description: apiError?.message || getErrorMessage(error) || 'Something went wrong', description: apiError?.message || getErrorMessage(error) || 'Something went wrong',
}); });
} finally {
setIsCodeSending(false);
} }
}, [ apiFetch, email, formApi, toast ]); }, [ apiFetch, email, executeRecaptcha, formApi, toast ]);
return ( return (
<FormProvider { ...formApi }> <FormProvider { ...formApi }>
...@@ -98,22 +105,27 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => { ...@@ -98,22 +105,27 @@ const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
<chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' } <chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' }
and enter your code below. and enter your code below.
</Text> </Text>
<AuthModalFieldOtpCode/> <AuthModalFieldOtpCode isDisabled={ isCodeSending }/>
<Link <Button
variant="link"
display="flex" display="flex"
alignItems="center" alignItems="center"
gap={ 2 } columnGap={ 2 }
mt={ 3 } mt={ 3 }
fontWeight="400"
w="fit-content" w="fit-content"
isDisabled={ isCodeSending }
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>
</Link> </Button>
<Button <Button
mt={ 6 } mt={ 6 }
type="submit" type="submit"
isLoading={ formApi.formState.isSubmitting } isLoading={ formApi.formState.isSubmitting }
isDisabled={ formApi.formState.isSubmitting || isCodeSending }
loadingText="Submit"
onClick={ formApi.handleSubmit(onFormSubmit) } onClick={ formApi.handleSubmit(onFormSubmit) }
> >
Submit Submit
......
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