Skip to content

Commit

Permalink
add OTP verification on login (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
ProgrammerDATCH authored Aug 4, 2024
1 parent 41c539c commit 9b26f3c
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 1 deletion.
125 changes: 124 additions & 1 deletion src/pages/UserLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -35,6 +39,7 @@ function UserLogin() {
token,
error,
message,
userId
} = useAppSelector((state) => state.auth);

const formik = useFormik({
Expand Down Expand Up @@ -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 (
<section className="section__login">
<div className="mini-container login">
Expand Down Expand Up @@ -186,6 +223,92 @@ function UserLogin() {
</p>
</div>
</div>

<Dialog
open={openOTPDialog}
onClose={() => setOpenOTPDialog(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
PaperProps={{
style: {
borderRadius: '12px',
padding: '24px',
maxWidth: '400px',
},
}}
>
<DialogTitle id="alert-dialog-title" sx={{ fontSize: '2rem', fontWeight: 'bold', color: '#ff6d18' }}>
Verify OTP
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description" sx={{ fontSize: '1.2rem', marginBottom: '20px' }}>
Please enter the 6-digit OTP sent to your email to verify your account.
</DialogContentText>
<Box sx={{ display: 'flex', justifyContent: 'center', gap: '8px', marginBottom: '20px' }}>
{otp.map((digit, index) => (
<TextField
key={index}
id={`otp-${index}`}
value={digit}
onChange={(e) => 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',
},
},
}}
/>
))}
</Box>
</DialogContent>
<DialogActions sx={{ justifyContent: 'center', gap: '16px' }}>
<Button
onClick={() => { setOpenOTPDialog(false); setOtp(['', '', '', '', '', '']); }}
sx={{
backgroundColor: '#f0f0f0',
color: '#333',
fontSize: '1.2rem',
padding: '8px 24px',
borderRadius: '8px',
'&:hover': {
backgroundColor: '#e0e0e0',
},
}}
>
Cancel
</Button>
<Button
onClick={handleVerifyOTP}
sx={{
backgroundColor: '#ff6d18',
color: '#fff',
fontSize: '1.2rem',
padding: '8px 24px',
borderRadius: '8px',
'&:hover': {
backgroundColor: '#e65b00',
},
}}
autoFocus
>
{isLoading ? "Verifying" : "Verify"}
<PulseLoader size={6} color="#ffe2d1" loading={isLoading} />
</Button>
</DialogActions>
</Dialog>

</section>
);
}
Expand Down
8 changes: 8 additions & 0 deletions src/store/features/auth/authService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -81,6 +88,7 @@ const authService = {
googleAuthCallback,
sendResetLink,
resetPassword,
verifyOTP,
};

export default authService;
37 changes: 37 additions & 0 deletions src/store/features/auth/authSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const initialState: AuthService = {
token: "",
isAuthenticated: false,
error: "",
userId: "",
};

type IUserEmailAndPassword = Pick<IUser, 'email' | 'password'>;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<any>) => {
state.isError = true;
Expand Down Expand Up @@ -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<any>) => {
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<any>) => {
state.isError = true;
state.isLoading = false;
state.isSuccess = false;
state.error = action.payload
})
},
});

Expand Down
1 change: 1 addition & 0 deletions src/utils/types/store.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface AuthService {
message: string;
error: string;
token: string;
userId?: any;
}

export interface IEmail {
Expand Down

0 comments on commit 9b26f3c

Please sign in to comment.