mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 19:28:16 +00:00
Improve OTP code modal on email change flow
This commit is contained in:
+81
-73
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import ReactCodeInput from "react-code-input";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useUser } from "@app/context";
|
||||||
import { useRequestEmailChangeOTP, useUpdateUserEmail } from "@app/hooks/api/users";
|
import { useRequestEmailChangeOTP, useUpdateUserEmail } from "@app/hooks/api/users";
|
||||||
import { clearSession } from "@app/hooks/api/users/queries";
|
import { clearSession } from "@app/hooks/api/users/queries";
|
||||||
@@ -16,19 +17,33 @@ const emailSchema = z
|
|||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
const otpSchema = z
|
|
||||||
.object({
|
|
||||||
otpCode: z.string().length(8, "OTP code must be exactly 8 digits")
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
export type EmailFormData = z.infer<typeof emailSchema>;
|
export type EmailFormData = z.infer<typeof emailSchema>;
|
||||||
export type OTPFormData = z.infer<typeof otpSchema>;
|
|
||||||
|
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 = () => {
|
export const ChangeEmailSection = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const [step, setStep] = useState<"email" | "otp">("email");
|
const [isOTPModalOpen, setIsOTPModalOpen] = useState(false);
|
||||||
const [pendingEmail, setPendingEmail] = useState("");
|
const [pendingEmail, setPendingEmail] = useState("");
|
||||||
|
|
||||||
const emailForm = useForm<EmailFormData>({
|
const emailForm = useForm<EmailFormData>({
|
||||||
@@ -36,11 +51,6 @@ export const ChangeEmailSection = () => {
|
|||||||
resolver: zodResolver(emailSchema)
|
resolver: zodResolver(emailSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const otpForm = useForm<OTPFormData>({
|
|
||||||
defaultValues: { otpCode: "" },
|
|
||||||
resolver: zodResolver(otpSchema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: requestEmailChangeOTP, isPending: isRequestingOTP } =
|
const { mutateAsync: requestEmailChangeOTP, isPending: isRequestingOTP } =
|
||||||
useRequestEmailChangeOTP();
|
useRequestEmailChangeOTP();
|
||||||
const { mutateAsync: updateUserEmail, isPending: isUpdatingEmail } = useUpdateUserEmail();
|
const { mutateAsync: updateUserEmail, isPending: isUpdatingEmail } = useUpdateUserEmail();
|
||||||
@@ -74,7 +84,7 @@ export const ChangeEmailSection = () => {
|
|||||||
try {
|
try {
|
||||||
await requestEmailChangeOTP({ newEmail });
|
await requestEmailChangeOTP({ newEmail });
|
||||||
setPendingEmail(newEmail);
|
setPendingEmail(newEmail);
|
||||||
setStep("otp");
|
setIsOTPModalOpen(true);
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Verification code sent to your new email address. Check your inbox!",
|
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 {
|
try {
|
||||||
await updateUserEmail({ newEmail: pendingEmail, otpCode });
|
await updateUserEmail({ newEmail: pendingEmail, otpCode: typedOTP });
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Email updated successfully. You will be redirected to login.",
|
text: "Email updated successfully. You will be redirected to login.",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset forms
|
// Reset forms and close modal
|
||||||
emailForm.reset();
|
emailForm.reset();
|
||||||
otpForm.reset();
|
setIsOTPModalOpen(false);
|
||||||
setStep("email");
|
|
||||||
setPendingEmail("");
|
setPendingEmail("");
|
||||||
|
setTypedOTP("");
|
||||||
|
|
||||||
// Clear frontend session/token to ensure proper logout
|
// Clear frontend session/token to ensure proper logout
|
||||||
clearSession(true);
|
clearSession(true);
|
||||||
@@ -118,10 +138,10 @@ export const ChangeEmailSection = () => {
|
|||||||
const errorMessage = err?.response?.data?.message || "Invalid verification code";
|
const errorMessage = err?.response?.data?.message || "Invalid verification code";
|
||||||
if (errorMessage.includes("Invalid verification code")) {
|
if (errorMessage.includes("Invalid verification code")) {
|
||||||
// Reset to email step so user must request new OTP
|
// Reset to email step so user must request new OTP
|
||||||
setStep("email");
|
setIsOTPModalOpen(false);
|
||||||
setPendingEmail("");
|
setPendingEmail("");
|
||||||
|
setTypedOTP("");
|
||||||
emailForm.reset();
|
emailForm.reset();
|
||||||
otpForm.reset();
|
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Invalid verification code. Please request a new one.",
|
text: "Invalid verification code. Please request a new one.",
|
||||||
@@ -136,11 +156,17 @@ export const ChangeEmailSection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOTPModalClose = () => {
|
||||||
|
setIsOTPModalOpen(false);
|
||||||
|
setPendingEmail("");
|
||||||
|
setTypedOTP("");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<>
|
||||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Change email</h2>
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Change email</h2>
|
||||||
|
|
||||||
{step === "email" ? (
|
|
||||||
<form onSubmit={emailForm.handleSubmit(handleEmailSubmit)}>
|
<form onSubmit={emailForm.handleSubmit(handleEmailSubmit)}>
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -174,64 +200,46 @@ export const ChangeEmailSection = () => {
|
|||||||
We'll send an 8-digit verification code to your new email address.
|
We'll send an 8-digit verification code to your new email address.
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
</div>
|
||||||
<div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<p className="text-sm text-mineshaft-300">
|
|
||||||
Enter the 8-digit verification code sent to: <b>{pendingEmail}</b>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={otpForm.handleSubmit(handleOTPSubmit)}>
|
<Modal
|
||||||
<div className="max-w-md">
|
isOpen={isOTPModalOpen}
|
||||||
<Controller
|
onOpenChange={(isOpen) => {
|
||||||
control={otpForm.control}
|
if (!isOpen) handleOTPModalClose();
|
||||||
name="otpCode"
|
}}
|
||||||
render={({ field, fieldState: { error } }) => (
|
>
|
||||||
<FormControl
|
<ModalContent
|
||||||
label="Verification code"
|
title="Email Verification"
|
||||||
isError={Boolean(error)}
|
subTitle={`Enter the 8-digit verification code sent to: ${pendingEmail}`}
|
||||||
errorText={error?.message}
|
>
|
||||||
>
|
<div className="flex flex-col items-center space-y-4">
|
||||||
<Input
|
<div className="flex justify-center">
|
||||||
{...field}
|
<ReactCodeInput
|
||||||
placeholder="Enter 8-digit code"
|
name="otp-input"
|
||||||
maxLength={8}
|
inputMode="tel"
|
||||||
className="bg-mineshaft-800 text-center font-mono"
|
type="text"
|
||||||
/>
|
fields={8}
|
||||||
</FormControl>
|
onChange={setTypedOTP}
|
||||||
)}
|
value={typedOTP}
|
||||||
|
{...otpInputProps}
|
||||||
|
className="mb-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
<div className="mt-4 flex gap-2">
|
<Button colorSchema="secondary" variant="outline" onClick={handleOTPModalClose}>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
setStep("email");
|
|
||||||
setPendingEmail("");
|
|
||||||
otpForm.reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
onClick={handleOTPSubmit}
|
||||||
colorSchema="primary"
|
|
||||||
isLoading={isUpdatingEmail}
|
isLoading={isUpdatingEmail}
|
||||||
isDisabled={isUpdatingEmail}
|
isDisabled={typedOTP.length !== 8}
|
||||||
>
|
>
|
||||||
Confirm Email Change
|
Confirm Email Change
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
</ModalContent>
|
||||||
<p className="mt-2 font-inter text-sm text-mineshaft-400">
|
</Modal>
|
||||||
After confirming, you'll be logged out and need to sign in again.
|
</>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user