diff --git a/src/hooks/useLoginQuery.ts b/src/hooks/useLoginQuery.ts index 7be4b5d..e62189f 100644 --- a/src/hooks/useLoginQuery.ts +++ b/src/hooks/useLoginQuery.ts @@ -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 })); } diff --git a/src/hooks/useUpdateUserEmailQuery.ts.ts b/src/hooks/useUpdateUserEmailQuery.ts.ts new file mode 100644 index 0000000..2038dd2 --- /dev/null +++ b/src/hooks/useUpdateUserEmailQuery.ts.ts @@ -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({ + 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; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 842724a..3f1f3b1 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -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; }; diff --git a/src/pages/profile/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx b/src/pages/profile/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx index 222c545..e16513c 100644 --- a/src/pages/profile/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx +++ b/src/pages/profile/containers/ConfirmEmailContainer/ConfirmEmailContainer.tsx @@ -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(false); + const [code, setCode] = useState(''); 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) => { + 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 ( = Enter the 4-digit code we send to {data.newEmail} - - - - - - - - - - - - - - - {expired > 0 ? ( - - Expires in {expired} seconds. - - ) : ( - - )} +
verifyCodeHandler(e)}> + + + + + setCode(code)}> + + + + + + + + + + {expired > 0 ? ( + + Code expires in {expired} seconds. + + ) : ( + + )} + - - + +
); }; diff --git a/src/pages/profile/containers/UpdateEmailContainer/UpdateEmailForm/UpdateEmailForm.tsx b/src/pages/profile/containers/UpdateEmailContainer/UpdateEmailForm/UpdateEmailForm.tsx index fbd50ca..eddbf66 100644 --- a/src/pages/profile/containers/UpdateEmailContainer/UpdateEmailForm/UpdateEmailForm.tsx +++ b/src/pages/profile/containers/UpdateEmailContainer/UpdateEmailForm/UpdateEmailForm.tsx @@ -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(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({ resolver: yupResolver(UpdateEmailSchema), }); - const toast = useToast(); - const handleError = useErrorHandler(); - const onSubmit: SubmitHandler = 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 = ({ email }) => { + setEmail(email); + sendConfirmationCode(email); }; return ( @@ -65,7 +60,15 @@ const UpdateEmailForm: React.FC = () => { - diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 47d3df4..d709d4f 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -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 { + public async updateEmail(email: string): Promise { const result = await this.api.patch('v1/me/email', { email }); return result.data; } - public async updateEmailConfirm(email: string, confirmationCode: number): Promise { + public async updateEmailConfirm(email: string, confirmationCode: string): Promise { const result = await this.api.post('v1/me/email/change/confirm', { email, confirmationCode, diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 551f120..cb18581 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -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 { + /** + * Send confirmation code on update user email + * + * @param email New email + * @returns Expires at code in seconds + */ + public async updateCurrentUserEmail(email: string): Promise { 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 { + try { + const result = await this.userService.updateEmailConfirm(email, confirmationCode); + return result; + } catch (error) { + throw error; + } + } + public reset(): void { runInAction(() => { this.me = null;