From 9b26f3cdd8a231d6d7aaec404e8513e3d6cc70d9 Mon Sep 17 00:00:00 2001 From: "Mr. David" <128073754+ProgrammerDATCH@users.noreply.github.com> Date: Mon, 5 Aug 2024 01:19:18 +0300 Subject: [PATCH] add OTP verification on login (#66) --- src/pages/UserLogin.tsx | 125 +++++++++++++++++++++++- src/store/features/auth/authService.tsx | 8 ++ src/store/features/auth/authSlice.tsx | 37 +++++++ src/utils/types/store.d.ts | 1 + 4 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/pages/UserLogin.tsx b/src/pages/UserLogin.tsx index f0c9979c..8687a981 100644 --- a/src/pages/UserLogin.tsx +++ b/src/pages/UserLogin.tsx @@ -4,7 +4,7 @@ import { FcGoogle } from 'react-icons/fc'; import { BiSolidShow } from 'react-icons/bi'; import { BiSolidHide } from 'react-icons/bi'; import { useAppDispatch, useAppSelector } from '../store/store'; -import { loginUser } from '../store/features/auth/authSlice'; +import { loginUser, verifyOTP } from '../store/features/auth/authSlice'; import { toast } from 'react-toastify'; import { useFormik } from 'formik'; import * as Yup from 'yup'; @@ -13,6 +13,8 @@ import { PulseLoader } from 'react-spinners'; import { addProductToWishlist } from '../store/features/wishlist/wishlistSlice'; import authService from '../store/features/auth/authService'; import { joinRoom } from '../utils/socket/socket'; +import { Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material'; +import { FaSpinner } from 'react-icons/fa'; const LoginSchema = Yup.object().shape({ email: Yup.string() @@ -25,6 +27,8 @@ function UserLogin() { const [isClicked, setIsClicked] = useState(false); const [isFocused, setIsFocused] = useState(false); const [isVisible, setIsVisible] = useState(false); + const [openOTPDialog, setOpenOTPDialog] = useState(false); + const [otp, setOtp] = useState(['', '', '', '', '', '']); const navigate = useNavigate(); const dispatch = useAppDispatch(); const { @@ -35,6 +39,7 @@ function UserLogin() { token, error, message, + userId } = useAppSelector((state) => state.auth); const formik = useFormik({ @@ -88,6 +93,38 @@ function UserLogin() { setIsVisible((isVisible) => !isVisible); } + useEffect(() => { + if (isSuccess && message === "Check your Email for OTP Confirmation") { + setOpenOTPDialog(true); + } + }, [isSuccess, message]); + + const handleOtpChange = (index, value) => { + const newOtp = [...otp]; + newOtp[index] = value; + setOtp(newOtp); + if (value && index < 5) { + const nextInput = document.getElementById(`otp-${index + 1}`); + if (nextInput) nextInput.focus(); + } + }; + + const handleVerifyOTP = async () => { + const otpString = otp.join(''); + if (otpString.length === 6) { + const res = await dispatch(verifyOTP({ userId, otp: otpString })); + if (res.type = 'auth/verify-otp/rejected') { + toast.error(res.payload) + } + else { + setOpenOTPDialog(false); + } + setOtp(['', '', '', '', '', '']) + } else { + toast.error("Please enter a valid 6-digit OTP"); + } + }; + return (
@@ -186,6 +223,92 @@ function UserLogin() {

+ + setOpenOTPDialog(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + PaperProps={{ + style: { + borderRadius: '12px', + padding: '24px', + maxWidth: '400px', + }, + }} + > + + Verify OTP + + + + Please enter the 6-digit OTP sent to your email to verify your account. + + + {otp.map((digit, index) => ( + handleOtpChange(index, e.target.value)} + inputProps={{ + maxLength: 1, + style: { textAlign: 'center', fontSize: '1.5rem' } + }} + sx={{ + width: '40px', + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: '#ff6d18', + }, + '&:hover fieldset': { + borderColor: '#e65b00', + }, + '&.Mui-focused fieldset': { + borderColor: '#e65b00', + }, + }, + }} + /> + ))} + + + + + + + +
); } diff --git a/src/store/features/auth/authService.tsx b/src/store/features/auth/authService.tsx index 145092de..27330ce1 100644 --- a/src/store/features/auth/authService.tsx +++ b/src/store/features/auth/authService.tsx @@ -69,6 +69,13 @@ const resetPassword = async (token: string, password: string) => { return response.data; }; +const verifyOTP = async (userId: string, otp: string) => { + const response = await axiosInstance.post( + `/api/auth/verify-otp/${userId}`, + { otp } + ); + return response.data; +}; const authService = { register, @@ -81,6 +88,7 @@ const authService = { googleAuthCallback, sendResetLink, resetPassword, + verifyOTP, }; export default authService; diff --git a/src/store/features/auth/authSlice.tsx b/src/store/features/auth/authSlice.tsx index a2239327..b12b86b1 100644 --- a/src/store/features/auth/authSlice.tsx +++ b/src/store/features/auth/authSlice.tsx @@ -16,6 +16,7 @@ const initialState: AuthService = { token: "", isAuthenticated: false, error: "", + userId: "", }; type IUserEmailAndPassword = Pick; @@ -140,6 +141,21 @@ export const logout = createAsyncThunk("auth/logout", async (_, thunkApi) => { } }); +export const verifyOTP = createAsyncThunk( + "auth/verify-otp", + async ( + { userId, otp }: { userId: string; otp: string }, + thunkApi + ) => { + try { + const response = await authService.verifyOTP(userId, otp); + return response; + } catch (error) { + return thunkApi.rejectWithValue(getErrorMessage(error)); + } + } +); + const userSlice = createSlice({ name: "auth", initialState, @@ -291,6 +307,7 @@ const userSlice = createSlice({ state.isSuccess = true; state.message = action.payload.message; state.token = action.payload.data.token; + state.userId = action.payload.data.userId || ""; }) .addCase(loginUser.rejected, (state, action: PayloadAction) => { state.isError = true; @@ -339,6 +356,26 @@ const userSlice = createSlice({ state.user = undefined; state.error = action.payload.message; }) + + .addCase(verifyOTP.pending, (state) => { + state.isError = false; + state.isLoading = true; + state.isSuccess = false; + }) + .addCase(verifyOTP.fulfilled, (state, action: PayloadAction) => { + state.isError = false; + state.isLoading = false; + state.isAuthenticated = true; + state.isSuccess = true; + state.message = action.payload.message; + state.token = action.payload.data.token; + }) + .addCase(verifyOTP.rejected, (state, action: PayloadAction) => { + state.isError = true; + state.isLoading = false; + state.isSuccess = false; + state.error = action.payload + }) }, }); diff --git a/src/utils/types/store.d.ts b/src/utils/types/store.d.ts index ee294f32..d9344ce7 100644 --- a/src/utils/types/store.d.ts +++ b/src/utils/types/store.d.ts @@ -76,6 +76,7 @@ export interface AuthService { message: string; error: string; token: string; + userId?: any; } export interface IEmail {