mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: verifying email change
This commit is contained in:
parent
da4335ce7f
commit
20e3b99ba8
@ -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 }));
|
||||
}
|
||||
|
||||
66
src/hooks/useUpdateUserEmailQuery.ts.ts
Normal file
66
src/hooks/useUpdateUserEmailQuery.ts.ts
Normal 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;
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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,42 +103,55 @@ const ConfirmEmailContainer: React.FC<{ data: ConfirmEmailContainerDataType }> =
|
||||
Enter the 4-digit code we send to <Text as='b'>{data.newEmail}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Flex alignItems='center' mt='10'>
|
||||
<Box textAlign='center' w='full'>
|
||||
<Flex justifyContent='center'>
|
||||
<HStack>
|
||||
<PinInput size='lg'>
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
<PinInputField />
|
||||
</PinInput>
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Button
|
||||
p='0 50px'
|
||||
colorScheme='blue'
|
||||
mt='4'
|
||||
type='submit'
|
||||
size='md'
|
||||
variant='outline'
|
||||
loadingText='Validating...'
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
<Box mt='1.5'>
|
||||
{expired > 0 ? (
|
||||
<Text as='small' color='red'>
|
||||
Expires in {expired} seconds.
|
||||
</Text>
|
||||
) : (
|
||||
<Button variant='link' size='sm' colorScheme='black' mt='3' textDecoration='underline'>
|
||||
Resend code
|
||||
</Button>
|
||||
)}
|
||||
<form onSubmit={e => verifyCodeHandler(e)}>
|
||||
<Flex alignItems='center' mt='10'>
|
||||
<Box textAlign='center' w='full'>
|
||||
<Flex justifyContent='center'>
|
||||
<HStack>
|
||||
<PinInput size='lg' variant='flushed' onComplete={code => setCode(code)}>
|
||||
<PinInputField required />
|
||||
<PinInputField required />
|
||||
<PinInputField required />
|
||||
<PinInputField required />
|
||||
</PinInput>
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Button
|
||||
p='0 50px'
|
||||
colorScheme='blue'
|
||||
mt='4'
|
||||
type='submit'
|
||||
size='md'
|
||||
variant='outline'
|
||||
isLoading={isVerifying}
|
||||
loadingText='Verifying...'
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
<Box mt='1.5'>
|
||||
{expired > 0 ? (
|
||||
<Text as='small' color='red'>
|
||||
Code expires in {expired} seconds.
|
||||
</Text>
|
||||
) : (
|
||||
<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>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 }) => {
|
||||
setStatusPageView({
|
||||
status: 'confirm-email',
|
||||
data: { confirmEmailData: { newEmail: email, expired_seconds: data.expired_seconds } },
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.response) {
|
||||
toast({ title: `${error.message}`, status: 'error' });
|
||||
} else {
|
||||
handleError(error);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (email !== null && result !== null) {
|
||||
setStatusPageView({
|
||||
status: 'confirm-email',
|
||||
data: { confirmEmailData: { newEmail: email, expired_seconds: result.data.expired_seconds } },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user