Skip to content

Commit 9b26f3c

Browse files
add OTP verification on login (#66)
1 parent 41c539c commit 9b26f3c

File tree

4 files changed

+170
-1
lines changed

4 files changed

+170
-1
lines changed

Diff for: src/pages/UserLogin.tsx

+124-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { FcGoogle } from 'react-icons/fc';
44
import { BiSolidShow } from 'react-icons/bi';
55
import { BiSolidHide } from 'react-icons/bi';
66
import { useAppDispatch, useAppSelector } from '../store/store';
7-
import { loginUser } from '../store/features/auth/authSlice';
7+
import { loginUser, verifyOTP } from '../store/features/auth/authSlice';
88
import { toast } from 'react-toastify';
99
import { useFormik } from 'formik';
1010
import * as Yup from 'yup';
@@ -13,6 +13,8 @@ import { PulseLoader } from 'react-spinners';
1313
import { addProductToWishlist } from '../store/features/wishlist/wishlistSlice';
1414
import authService from '../store/features/auth/authService';
1515
import { joinRoom } from '../utils/socket/socket';
16+
import { Box, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material';
17+
import { FaSpinner } from 'react-icons/fa';
1618

1719
const LoginSchema = Yup.object().shape({
1820
email: Yup.string()
@@ -25,6 +27,8 @@ function UserLogin() {
2527
const [isClicked, setIsClicked] = useState(false);
2628
const [isFocused, setIsFocused] = useState(false);
2729
const [isVisible, setIsVisible] = useState(false);
30+
const [openOTPDialog, setOpenOTPDialog] = useState(false);
31+
const [otp, setOtp] = useState(['', '', '', '', '', '']);
2832
const navigate = useNavigate();
2933
const dispatch = useAppDispatch();
3034
const {
@@ -35,6 +39,7 @@ function UserLogin() {
3539
token,
3640
error,
3741
message,
42+
userId
3843
} = useAppSelector((state) => state.auth);
3944

4045
const formik = useFormik({
@@ -88,6 +93,38 @@ function UserLogin() {
8893
setIsVisible((isVisible) => !isVisible);
8994
}
9095

96+
useEffect(() => {
97+
if (isSuccess && message === "Check your Email for OTP Confirmation") {
98+
setOpenOTPDialog(true);
99+
}
100+
}, [isSuccess, message]);
101+
102+
const handleOtpChange = (index, value) => {
103+
const newOtp = [...otp];
104+
newOtp[index] = value;
105+
setOtp(newOtp);
106+
if (value && index < 5) {
107+
const nextInput = document.getElementById(`otp-${index + 1}`);
108+
if (nextInput) nextInput.focus();
109+
}
110+
};
111+
112+
const handleVerifyOTP = async () => {
113+
const otpString = otp.join('');
114+
if (otpString.length === 6) {
115+
const res = await dispatch(verifyOTP({ userId, otp: otpString }));
116+
if (res.type = 'auth/verify-otp/rejected') {
117+
toast.error(res.payload)
118+
}
119+
else {
120+
setOpenOTPDialog(false);
121+
}
122+
setOtp(['', '', '', '', '', ''])
123+
} else {
124+
toast.error("Please enter a valid 6-digit OTP");
125+
}
126+
};
127+
91128
return (
92129
<section className="section__login">
93130
<div className="mini-container login">
@@ -186,6 +223,92 @@ function UserLogin() {
186223
</p>
187224
</div>
188225
</div>
226+
227+
<Dialog
228+
open={openOTPDialog}
229+
onClose={() => setOpenOTPDialog(false)}
230+
aria-labelledby="alert-dialog-title"
231+
aria-describedby="alert-dialog-description"
232+
PaperProps={{
233+
style: {
234+
borderRadius: '12px',
235+
padding: '24px',
236+
maxWidth: '400px',
237+
},
238+
}}
239+
>
240+
<DialogTitle id="alert-dialog-title" sx={{ fontSize: '2rem', fontWeight: 'bold', color: '#ff6d18' }}>
241+
Verify OTP
242+
</DialogTitle>
243+
<DialogContent>
244+
<DialogContentText id="alert-dialog-description" sx={{ fontSize: '1.2rem', marginBottom: '20px' }}>
245+
Please enter the 6-digit OTP sent to your email to verify your account.
246+
</DialogContentText>
247+
<Box sx={{ display: 'flex', justifyContent: 'center', gap: '8px', marginBottom: '20px' }}>
248+
{otp.map((digit, index) => (
249+
<TextField
250+
key={index}
251+
id={`otp-${index}`}
252+
value={digit}
253+
onChange={(e) => handleOtpChange(index, e.target.value)}
254+
inputProps={{
255+
maxLength: 1,
256+
style: { textAlign: 'center', fontSize: '1.5rem' }
257+
}}
258+
sx={{
259+
width: '40px',
260+
'& .MuiOutlinedInput-root': {
261+
'& fieldset': {
262+
borderColor: '#ff6d18',
263+
},
264+
'&:hover fieldset': {
265+
borderColor: '#e65b00',
266+
},
267+
'&.Mui-focused fieldset': {
268+
borderColor: '#e65b00',
269+
},
270+
},
271+
}}
272+
/>
273+
))}
274+
</Box>
275+
</DialogContent>
276+
<DialogActions sx={{ justifyContent: 'center', gap: '16px' }}>
277+
<Button
278+
onClick={() => { setOpenOTPDialog(false); setOtp(['', '', '', '', '', '']); }}
279+
sx={{
280+
backgroundColor: '#f0f0f0',
281+
color: '#333',
282+
fontSize: '1.2rem',
283+
padding: '8px 24px',
284+
borderRadius: '8px',
285+
'&:hover': {
286+
backgroundColor: '#e0e0e0',
287+
},
288+
}}
289+
>
290+
Cancel
291+
</Button>
292+
<Button
293+
onClick={handleVerifyOTP}
294+
sx={{
295+
backgroundColor: '#ff6d18',
296+
color: '#fff',
297+
fontSize: '1.2rem',
298+
padding: '8px 24px',
299+
borderRadius: '8px',
300+
'&:hover': {
301+
backgroundColor: '#e65b00',
302+
},
303+
}}
304+
autoFocus
305+
>
306+
{isLoading ? "Verifying" : "Verify"}
307+
<PulseLoader size={6} color="#ffe2d1" loading={isLoading} />
308+
</Button>
309+
</DialogActions>
310+
</Dialog>
311+
189312
</section>
190313
);
191314
}

Diff for: src/store/features/auth/authService.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ const resetPassword = async (token: string, password: string) => {
6969
return response.data;
7070
};
7171

72+
const verifyOTP = async (userId: string, otp: string) => {
73+
const response = await axiosInstance.post(
74+
`/api/auth/verify-otp/${userId}`,
75+
{ otp }
76+
);
77+
return response.data;
78+
};
7279

7380
const authService = {
7481
register,
@@ -81,6 +88,7 @@ const authService = {
8188
googleAuthCallback,
8289
sendResetLink,
8390
resetPassword,
91+
verifyOTP,
8492
};
8593

8694
export default authService;

Diff for: src/store/features/auth/authSlice.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const initialState: AuthService = {
1616
token: "",
1717
isAuthenticated: false,
1818
error: "",
19+
userId: "",
1920
};
2021

2122
type IUserEmailAndPassword = Pick<IUser, 'email' | 'password'>;
@@ -140,6 +141,21 @@ export const logout = createAsyncThunk("auth/logout", async (_, thunkApi) => {
140141
}
141142
});
142143

144+
export const verifyOTP = createAsyncThunk(
145+
"auth/verify-otp",
146+
async (
147+
{ userId, otp }: { userId: string; otp: string },
148+
thunkApi
149+
) => {
150+
try {
151+
const response = await authService.verifyOTP(userId, otp);
152+
return response;
153+
} catch (error) {
154+
return thunkApi.rejectWithValue(getErrorMessage(error));
155+
}
156+
}
157+
);
158+
143159
const userSlice = createSlice({
144160
name: "auth",
145161
initialState,
@@ -291,6 +307,7 @@ const userSlice = createSlice({
291307
state.isSuccess = true;
292308
state.message = action.payload.message;
293309
state.token = action.payload.data.token;
310+
state.userId = action.payload.data.userId || "";
294311
})
295312
.addCase(loginUser.rejected, (state, action: PayloadAction<any>) => {
296313
state.isError = true;
@@ -339,6 +356,26 @@ const userSlice = createSlice({
339356
state.user = undefined;
340357
state.error = action.payload.message;
341358
})
359+
360+
.addCase(verifyOTP.pending, (state) => {
361+
state.isError = false;
362+
state.isLoading = true;
363+
state.isSuccess = false;
364+
})
365+
.addCase(verifyOTP.fulfilled, (state, action: PayloadAction<any>) => {
366+
state.isError = false;
367+
state.isLoading = false;
368+
state.isAuthenticated = true;
369+
state.isSuccess = true;
370+
state.message = action.payload.message;
371+
state.token = action.payload.data.token;
372+
})
373+
.addCase(verifyOTP.rejected, (state, action: PayloadAction<any>) => {
374+
state.isError = true;
375+
state.isLoading = false;
376+
state.isSuccess = false;
377+
state.error = action.payload
378+
})
342379
},
343380
});
344381

Diff for: src/utils/types/store.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface AuthService {
7676
message: string;
7777
error: string;
7878
token: string;
79+
userId?: any;
7980
}
8081

8182
export interface IEmail {

0 commit comments

Comments
 (0)