feat: verifying email change

This commit is contained in:
Sergey Yarkov 2021-09-07 23:26:52 +03:00
parent da4335ce7f
commit 20e3b99ba8
7 changed files with 244 additions and 65 deletions

View File

@ -33,7 +33,7 @@ const useLoginQuery = () => {
if (isMountedRef.current) {
setState(s => ({ ...s, result }));
}
} catch (error) {
} catch (error: any) {
if (isMountedRef.current) {
setState(s => ({ ...s, error }));
}

View File

@ -0,0 +1,66 @@
import { useToast } from '@chakra-ui/toast';
import { ISentCodeResult } from 'interfaces';
import { useState } from 'react';
import { useErrorHandler } from 'react-error-boundary';
import useIsMountedRef from './useIsMountedRef';
import { useRootStore } from './useRootStore';
type UpdateUserEmailStateType = {
result: ISentCodeResult | null;
error: any;
loading: boolean;
fetched: boolean;
};
const useUpdateUserEmailQuery = () => {
const isMountedRef = useIsMountedRef();
const { userStore } = useRootStore();
const [state, setState] = useState<UpdateUserEmailStateType>({
result: null,
error: null,
loading: false,
fetched: false,
});
const toast = useToast();
const handleError = useErrorHandler();
const sendConfirmationCode = async (email: string) => {
try {
setState(s => ({ ...s, loading: true }));
const result = await userStore.updateCurrentUserEmail(email);
toast({ title: 'Confirmation code has been sent.', status: 'info' });
if (isMountedRef.current) {
setState(s => ({ ...s, result }));
}
return result;
} catch (error: any) {
if (isMountedRef.current) {
setState(s => ({ ...s, error }));
}
if (error.response) {
switch (error.response.status) {
case 503:
toast({ title: 'Service unavailable. Please, try later again.', status: 'error' });
break;
default:
handleError(error);
break;
}
} else {
console.error(error);
handleError(error);
}
} finally {
if (isMountedRef.current) {
setState(s => ({ ...s, loading: false, fetched: true }));
}
}
};
return { ...state, sendConfirmationCode };
};
export default useUpdateUserEmailQuery;

View File

@ -52,7 +52,13 @@ export interface IUpdatedContactsResult extends IDataResult {
data: IUserContacts;
}
export interface IUpdatedEmailResult extends IDataResult {
export interface IUpdatedUserEmail extends IDataResult {
data: {
email: IUser['email'];
};
}
export interface ISentCodeResult extends IDataResult {
data: {
expired_seconds: number;
};

View File

@ -1,11 +1,25 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { PinInput, PinInputField } from '@chakra-ui/react';
import { Button } from '@chakra-ui/button';
import { Box, Flex, Heading, HStack, Text } from '@chakra-ui/layout';
import { ConfirmEmailContainerDataType } from 'types';
import useUpdateUserEmailQuery from 'hooks/useUpdateUserEmailQuery.ts';
import { useRootStore } from 'hooks/useRootStore';
import { useToast } from '@chakra-ui/toast';
import { useErrorHandler } from 'react-error-boundary';
import { ProfilePageViewContext } from 'contexts';
import useIsMountedRef from 'hooks/useIsMountedRef';
const ConfirmEmailContainer: React.FC<{ data: ConfirmEmailContainerDataType }> = ({ data }) => {
const { userStore } = useRootStore();
const { setStatusPageView } = useContext(ProfilePageViewContext);
const { sendConfirmationCode, loading } = useUpdateUserEmailQuery();
const [isVerifying, setIsVerifying] = useState<boolean>(false);
const [code, setCode] = useState<string>('');
const [expired, setExpired] = useState(data.expired_seconds);
const isMountedRef = useIsMountedRef();
const toast = useToast();
const handleError = useErrorHandler();
useEffect(() => {
const interval = window.setInterval(() => {
@ -17,7 +31,61 @@ const ConfirmEmailContainer: React.FC<{ data: ConfirmEmailContainerDataType }> =
}, 1000);
return () => window.clearInterval(interval);
}, [expired]);
const verifyCodeHandler = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
setIsVerifying(true);
await userStore.updateCurrentUserEmailConfirm(data.newEmail, code);
toast({
title: 'Email updated.',
status: 'info',
});
if (isMountedRef.current) {
setStatusPageView({ status: 'default' });
}
} catch (error: any) {
if (error.response) {
switch (error.response.status) {
/**
* Invalid confirmation code
*/
case 400:
toast({ title: 'Invalid code.', status: 'error' });
break;
/**
* Code confirmation is expired
*/
case 404:
toast({
title: 'Looks like code has expired.',
description: 'Try send new code again.',
status: 'warning',
});
break;
default:
handleError(error);
break;
}
} else {
handleError(error);
}
} finally {
setIsVerifying(false);
}
};
const resendCodeHandler = async () => {
const result = await sendConfirmationCode(data.newEmail);
if (result !== undefined) {
/**
* Reset timer state
*/
const { expired_seconds } = result.data;
setExpired(v => (v = expired_seconds));
}
};
return (
<Box
@ -35,15 +103,16 @@ const ConfirmEmailContainer: React.FC<{ data: ConfirmEmailContainerDataType }> =
Enter the 4-digit code we send to <Text as='b'>{data.newEmail}</Text>
</Text>
</Box>
<form onSubmit={e => verifyCodeHandler(e)}>
<Flex alignItems='center' mt='10'>
<Box textAlign='center' w='full'>
<Flex justifyContent='center'>
<HStack>
<PinInput size='lg'>
<PinInputField />
<PinInputField />
<PinInputField />
<PinInputField />
<PinInput size='lg' variant='flushed' onComplete={code => setCode(code)}>
<PinInputField required />
<PinInputField required />
<PinInputField required />
<PinInputField required />
</PinInput>
</HStack>
</Flex>
@ -54,23 +123,35 @@ const ConfirmEmailContainer: React.FC<{ data: ConfirmEmailContainerDataType }> =
type='submit'
size='md'
variant='outline'
loadingText='Validating...'
isLoading={isVerifying}
loadingText='Verifying...'
>
Verify
</Button>
<Box mt='1.5'>
{expired > 0 ? (
<Text as='small' color='red'>
Expires in {expired} seconds.
Code expires in {expired} seconds.
</Text>
) : (
<Button variant='link' size='sm' colorScheme='black' mt='3' textDecoration='underline'>
<Button
type='submit'
onClick={resendCodeHandler}
variant='link'
size='sm'
colorScheme='black'
mt='3'
textDecoration='underline'
loadingText='Wait a second.'
isLoading={loading}
>
Resend code
</Button>
)}
</Box>
</Box>
</Flex>
</form>
</Box>
);
};

View File

@ -2,12 +2,11 @@ import { Button } from '@chakra-ui/button';
import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/form-control';
import { Input } from '@chakra-ui/input';
import { Box, Stack } from '@chakra-ui/layout';
import { useToast } from '@chakra-ui/toast';
import { yupResolver } from '@hookform/resolvers/yup';
import { ProfilePageViewContext } from 'contexts';
import { useRootStore } from 'hooks/useRootStore';
import React, { useContext } from 'react';
import { useErrorHandler } from 'react-error-boundary';
import useUpdateUserEmailQuery from 'hooks/useUpdateUserEmailQuery.ts';
import React, { useContext, useEffect, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import UpdateEmailSchema from './UpdateEmailForm.validator';
@ -19,8 +18,10 @@ type UpdateEmailInputType = {
* Update email using form
*/
const UpdateEmailForm: React.FC = () => {
const [email, setEmail] = useState<string | null>(null);
const { setStatusPageView } = useContext(ProfilePageViewContext);
const { userStore } = useRootStore();
const { sendConfirmationCode, loading, result } = useUpdateUserEmailQuery();
const {
handleSubmit,
register,
@ -28,25 +29,19 @@ const UpdateEmailForm: React.FC = () => {
} = useForm<UpdateEmailInputType>({
resolver: yupResolver(UpdateEmailSchema),
});
const toast = useToast();
const handleError = useErrorHandler();
const onSubmit: SubmitHandler<UpdateEmailInputType> = async ({ email }) => {
userStore
.updateUserEmail(email)
.then(({ data }) => {
useEffect(() => {
if (email !== null && result !== null) {
setStatusPageView({
status: 'confirm-email',
data: { confirmEmailData: { newEmail: email, expired_seconds: data.expired_seconds } },
data: { confirmEmailData: { newEmail: email, expired_seconds: result.data.expired_seconds } },
});
})
.catch(error => {
if (error.response) {
toast({ title: `${error.message}`, status: 'error' });
} else {
handleError(error);
}
});
const onSubmit: SubmitHandler<UpdateEmailInputType> = ({ email }) => {
setEmail(email);
sendConfirmationCode(email);
};
return (
@ -65,7 +60,15 @@ const UpdateEmailForm: React.FC = () => {
</FormHelperText>
</FormControl>
</Stack>
<Button colorScheme='blue' mt='4' type='submit' size='md' variant='outline' loadingText='Saving...'>
<Button
colorScheme='blue'
mt='4'
type='submit'
size='md'
variant='outline'
isLoading={loading}
loadingText='Sending...'
>
Send code
</Button>
</form>

View File

@ -1,5 +1,12 @@
import { AxiosInstance } from 'axios';
import { IDataResult, IUpdatedContactsResult, IUpdatedEmailResult, IUserContacts, IUserResult } from 'interfaces';
import {
IDataResult,
ISentCodeResult,
IUpdatedContactsResult,
IUpdatedUserEmail,
IUserContacts,
IUserResult,
} from 'interfaces';
import * as helpers from 'helpers';
export default class UserService {
@ -45,12 +52,12 @@ export default class UserService {
return result.data;
}
public async updateEmail(email: string): Promise<IUpdatedEmailResult> {
public async updateEmail(email: string): Promise<ISentCodeResult> {
const result = await this.api.patch('v1/me/email', { email });
return result.data;
}
public async updateEmailConfirm(email: string, confirmationCode: number): Promise<IDataResult> {
public async updateEmailConfirm(email: string, confirmationCode: string): Promise<IUpdatedUserEmail> {
const result = await this.api.post('v1/me/email/change/confirm', {
email,
confirmationCode,

View File

@ -6,7 +6,8 @@ import {
IUserContacts,
IUpdatedContactsResult,
IDataResult,
IUpdatedEmailResult,
ISentCodeResult,
IUpdatedUserEmail,
} from 'interfaces';
/**
@ -84,7 +85,13 @@ export default class UserStore {
}
}
public async updateUserEmail(email: string): Promise<IUpdatedEmailResult> {
/**
* Send confirmation code on update user email
*
* @param email New email
* @returns Expires at code in seconds
*/
public async updateCurrentUserEmail(email: string): Promise<ISentCodeResult> {
try {
const result = await this.userService.updateEmail(email);
return result;
@ -93,6 +100,15 @@ export default class UserStore {
}
}
public async updateCurrentUserEmailConfirm(email: string, confirmationCode: string): Promise<IUpdatedUserEmail> {
try {
const result = await this.userService.updateEmailConfirm(email, confirmationCode);
return result;
} catch (error) {
throw error;
}
}
public reset(): void {
runInAction(() => {
this.me = null;