mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: verifying email change
This commit is contained in:
parent
da4335ce7f
commit
20e3b99ba8
@ -33,7 +33,7 @@ const useLoginQuery = () => {
|
|||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
setState(s => ({ ...s, result }));
|
setState(s => ({ ...s, result }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
setState(s => ({ ...s, error }));
|
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;
|
data: IUserContacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdatedEmailResult extends IDataResult {
|
export interface IUpdatedUserEmail extends IDataResult {
|
||||||
|
data: {
|
||||||
|
email: IUser['email'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISentCodeResult extends IDataResult {
|
||||||
data: {
|
data: {
|
||||||
expired_seconds: number;
|
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 { PinInput, PinInputField } from '@chakra-ui/react';
|
||||||
import { Button } from '@chakra-ui/button';
|
import { Button } from '@chakra-ui/button';
|
||||||
import { Box, Flex, Heading, HStack, Text } from '@chakra-ui/layout';
|
import { Box, Flex, Heading, HStack, Text } from '@chakra-ui/layout';
|
||||||
import { ConfirmEmailContainerDataType } from 'types';
|
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 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 [expired, setExpired] = useState(data.expired_seconds);
|
||||||
|
const isMountedRef = useIsMountedRef();
|
||||||
|
const toast = useToast();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = window.setInterval(() => {
|
const interval = window.setInterval(() => {
|
||||||
@ -17,7 +31,61 @@ const ConfirmEmailContainer: React.FC<{ data: ConfirmEmailContainerDataType }> =
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => window.clearInterval(interval);
|
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 (
|
return (
|
||||||
<Box
|
<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>
|
Enter the 4-digit code we send to <Text as='b'>{data.newEmail}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
<form onSubmit={e => verifyCodeHandler(e)}>
|
||||||
<Flex alignItems='center' mt='10'>
|
<Flex alignItems='center' mt='10'>
|
||||||
<Box textAlign='center' w='full'>
|
<Box textAlign='center' w='full'>
|
||||||
<Flex justifyContent='center'>
|
<Flex justifyContent='center'>
|
||||||
<HStack>
|
<HStack>
|
||||||
<PinInput size='lg'>
|
<PinInput size='lg' variant='flushed' onComplete={code => setCode(code)}>
|
||||||
<PinInputField />
|
<PinInputField required />
|
||||||
<PinInputField />
|
<PinInputField required />
|
||||||
<PinInputField />
|
<PinInputField required />
|
||||||
<PinInputField />
|
<PinInputField required />
|
||||||
</PinInput>
|
</PinInput>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -54,23 +123,35 @@ const ConfirmEmailContainer: React.FC<{ data: ConfirmEmailContainerDataType }> =
|
|||||||
type='submit'
|
type='submit'
|
||||||
size='md'
|
size='md'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
loadingText='Validating...'
|
isLoading={isVerifying}
|
||||||
|
loadingText='Verifying...'
|
||||||
>
|
>
|
||||||
Verify
|
Verify
|
||||||
</Button>
|
</Button>
|
||||||
<Box mt='1.5'>
|
<Box mt='1.5'>
|
||||||
{expired > 0 ? (
|
{expired > 0 ? (
|
||||||
<Text as='small' color='red'>
|
<Text as='small' color='red'>
|
||||||
Expires in {expired} seconds.
|
Code expires in {expired} seconds.
|
||||||
</Text>
|
</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
|
Resend code
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</form>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,12 +2,11 @@ import { Button } from '@chakra-ui/button';
|
|||||||
import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/form-control';
|
import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/form-control';
|
||||||
import { Input } from '@chakra-ui/input';
|
import { Input } from '@chakra-ui/input';
|
||||||
import { Box, Stack } from '@chakra-ui/layout';
|
import { Box, Stack } from '@chakra-ui/layout';
|
||||||
import { useToast } from '@chakra-ui/toast';
|
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { ProfilePageViewContext } from 'contexts';
|
import { ProfilePageViewContext } from 'contexts';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
import React, { useContext } from 'react';
|
import useUpdateUserEmailQuery from 'hooks/useUpdateUserEmailQuery.ts';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||||
import UpdateEmailSchema from './UpdateEmailForm.validator';
|
import UpdateEmailSchema from './UpdateEmailForm.validator';
|
||||||
|
|
||||||
@ -19,8 +18,10 @@ type UpdateEmailInputType = {
|
|||||||
* Update email using form
|
* Update email using form
|
||||||
*/
|
*/
|
||||||
const UpdateEmailForm: React.FC = () => {
|
const UpdateEmailForm: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState<string | null>(null);
|
||||||
const { setStatusPageView } = useContext(ProfilePageViewContext);
|
const { setStatusPageView } = useContext(ProfilePageViewContext);
|
||||||
const { userStore } = useRootStore();
|
const { userStore } = useRootStore();
|
||||||
|
const { sendConfirmationCode, loading, result } = useUpdateUserEmailQuery();
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
@ -28,25 +29,19 @@ const UpdateEmailForm: React.FC = () => {
|
|||||||
} = useForm<UpdateEmailInputType>({
|
} = useForm<UpdateEmailInputType>({
|
||||||
resolver: yupResolver(UpdateEmailSchema),
|
resolver: yupResolver(UpdateEmailSchema),
|
||||||
});
|
});
|
||||||
const toast = useToast();
|
|
||||||
const handleError = useErrorHandler();
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<UpdateEmailInputType> = async ({ email }) => {
|
useEffect(() => {
|
||||||
userStore
|
if (email !== null && result !== null) {
|
||||||
.updateUserEmail(email)
|
|
||||||
.then(({ data }) => {
|
|
||||||
setStatusPageView({
|
setStatusPageView({
|
||||||
status: 'confirm-email',
|
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 (
|
return (
|
||||||
@ -65,7 +60,15 @@ const UpdateEmailForm: React.FC = () => {
|
|||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Stack>
|
</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
|
Send code
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import { AxiosInstance } from 'axios';
|
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';
|
import * as helpers from 'helpers';
|
||||||
|
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
@ -45,12 +52,12 @@ export default class UserService {
|
|||||||
return result.data;
|
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 });
|
const result = await this.api.patch('v1/me/email', { email });
|
||||||
return result.data;
|
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', {
|
const result = await this.api.post('v1/me/email/change/confirm', {
|
||||||
email,
|
email,
|
||||||
confirmationCode,
|
confirmationCode,
|
||||||
|
|||||||
@ -6,7 +6,8 @@ import {
|
|||||||
IUserContacts,
|
IUserContacts,
|
||||||
IUpdatedContactsResult,
|
IUpdatedContactsResult,
|
||||||
IDataResult,
|
IDataResult,
|
||||||
IUpdatedEmailResult,
|
ISentCodeResult,
|
||||||
|
IUpdatedUserEmail,
|
||||||
} from 'interfaces';
|
} 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 {
|
try {
|
||||||
const result = await this.userService.updateEmail(email);
|
const result = await this.userService.updateEmail(email);
|
||||||
return result;
|
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 {
|
public reset(): void {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.me = null;
|
this.me = null;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user