diff --git a/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx index 93d110705..92480d5bd 100644 --- a/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx +++ b/frontend/src/pages/user/PersonalSettingsPage/components/ChangeEmailSection/ChangeEmailSection.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; +import ReactCodeInput from "react-code-input"; import { Controller, useForm, useWatch } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useNavigate } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, Input } from "@app/components/v2"; +import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { useUser } from "@app/context"; import { useRequestEmailChangeOTP, useUpdateUserEmail } from "@app/hooks/api/users"; import { clearSession } from "@app/hooks/api/users/queries"; @@ -16,19 +17,33 @@ const emailSchema = z }) .required(); -const otpSchema = z - .object({ - otpCode: z.string().length(8, "OTP code must be exactly 8 digits") - }) - .required(); - export type EmailFormData = z.infer; -export type OTPFormData = z.infer; + +const otpInputProps = { + inputStyle: { + fontFamily: "monospace", + margin: "4px", + MozAppearance: "textfield" as const, + width: "45px", + borderRadius: "6px", + fontSize: "18px", + height: "45px", + padding: "0", + paddingLeft: "0", + paddingRight: "0", + backgroundColor: "#262626", + color: "white", + border: "1px solid #404040", + textAlign: "center" as const, + outlineColor: "#8ca542", + borderColor: "#404040" + } +}; export const ChangeEmailSection = () => { const navigate = useNavigate(); const { user } = useUser(); - const [step, setStep] = useState<"email" | "otp">("email"); + const [isOTPModalOpen, setIsOTPModalOpen] = useState(false); const [pendingEmail, setPendingEmail] = useState(""); const emailForm = useForm({ @@ -36,11 +51,6 @@ export const ChangeEmailSection = () => { resolver: zodResolver(emailSchema) }); - const otpForm = useForm({ - defaultValues: { otpCode: "" }, - resolver: zodResolver(otpSchema) - }); - const { mutateAsync: requestEmailChangeOTP, isPending: isRequestingOTP } = useRequestEmailChangeOTP(); const { mutateAsync: updateUserEmail, isPending: isUpdatingEmail } = useUpdateUserEmail(); @@ -74,7 +84,7 @@ export const ChangeEmailSection = () => { try { await requestEmailChangeOTP({ newEmail }); setPendingEmail(newEmail); - setStep("otp"); + setIsOTPModalOpen(true); createNotification({ text: "Verification code sent to your new email address. Check your inbox!", @@ -90,20 +100,30 @@ export const ChangeEmailSection = () => { } }; - const handleOTPSubmit = async ({ otpCode }: OTPFormData) => { + const [typedOTP, setTypedOTP] = useState(""); + + const handleOTPSubmit = async () => { + if (typedOTP.length !== 8) { + createNotification({ + text: "Please enter the complete 8-digit verification code", + type: "error" + }); + return; + } + try { - await updateUserEmail({ newEmail: pendingEmail, otpCode }); + await updateUserEmail({ newEmail: pendingEmail, otpCode: typedOTP }); createNotification({ text: "Email updated successfully. You will be redirected to login.", type: "success" }); - // Reset forms + // Reset forms and close modal emailForm.reset(); - otpForm.reset(); - setStep("email"); + setIsOTPModalOpen(false); setPendingEmail(""); + setTypedOTP(""); // Clear frontend session/token to ensure proper logout clearSession(true); @@ -118,10 +138,10 @@ export const ChangeEmailSection = () => { const errorMessage = err?.response?.data?.message || "Invalid verification code"; if (errorMessage.includes("Invalid verification code")) { // Reset to email step so user must request new OTP - setStep("email"); + setIsOTPModalOpen(false); setPendingEmail(""); + setTypedOTP(""); emailForm.reset(); - otpForm.reset(); createNotification({ text: "Invalid verification code. Please request a new one.", @@ -136,11 +156,17 @@ export const ChangeEmailSection = () => { } }; - return ( -
-

Change email

+ const handleOTPModalClose = () => { + setIsOTPModalOpen(false); + setPendingEmail(""); + setTypedOTP(""); + }; + + return ( + <> +
+

Change email

- {step === "email" ? (
{ We'll send an 8-digit verification code to your new email address.

- ) : ( -
-
-

- Enter the 8-digit verification code sent to: {pendingEmail} -

-
+
-
-
- ( - - - - )} + { + if (!isOpen) handleOTPModalClose(); + }} + > + +
+
+
- -
-
- - -

- After confirming, you'll be logged out and need to sign in again. -

-
- )} -
+
+ + + ); };