Refactor 2FA enablement flow in Enable2FA component

- Simplified the password submission and verification processes.
- Introduced a new state for OTP input, allowing for direct user input.
- Updated error handling to provide clearer feedback during the verification process.
- Enhanced the user experience by resetting the OTP value when switching steps and modifying the form structure for better clarity.
This commit is contained in:
Mauricio Siu 2025-04-17 00:25:27 -06:00
parent 8fbad8a26e
commit 48cfe66a6b

View File

@ -61,6 +61,79 @@ export const Enable2FA = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [step, setStep] = useState<"password" | "verify">("password"); const [step, setStep] = useState<"password" | "verify">("password");
const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [otpValue, setOtpValue] = useState("");
const handleVerifySubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const result = await authClient.twoFactor.verifyTotp({
code: otpValue,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
toast.error(errorMessage);
} else {
toast.error("Error verifying 2FA code", {
description: error instanceof Error ? error.message : "Unknown error",
});
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
setOtpValue("");
passwordForm.reset({
password: "",
issuer: "",
});
}
}, [isDialogOpen, passwordForm]);
useEffect(() => {
if (step === "verify") {
setOtpValue("");
}
}, [step]);
const handlePasswordSubmit = async (formData: PasswordForm) => { const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true); setIsPasswordLoading(true);
@ -105,75 +178,6 @@ export const Enable2FA = () => {
} }
}; };
const handleVerifySubmit = async (formData: PinForm) => {
try {
const result = await authClient.twoFactor.verifyTotp({
code: formData.pin,
});
if (result.error) {
if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") {
pinForm.setError("pin", {
message: "Invalid code. Please try again.",
});
toast.error("Invalid verification code");
return;
}
throw result.error;
}
if (!result.data) {
throw new Error("No response received from server");
}
toast.success("2FA configured successfully");
utils.user.get.invalidate();
setIsDialogOpen(false);
} catch (error) {
if (error instanceof Error) {
const errorMessage =
error.message === "Failed to fetch"
? "Connection error. Please check your internet connection."
: error.message;
pinForm.setError("pin", {
message: errorMessage,
});
toast.error(errorMessage);
} else {
pinForm.setError("pin", {
message: "Error verifying code",
});
toast.error("Error verifying 2FA code");
}
}
};
const passwordForm = useForm<PasswordForm>({
resolver: zodResolver(PasswordSchema),
defaultValues: {
password: "",
},
});
const pinForm = useForm<PinForm>({
resolver: zodResolver(PinSchema),
defaultValues: {
pin: "",
},
});
useEffect(() => {
if (!isDialogOpen) {
setStep("password");
setData(null);
setBackupCodes([]);
passwordForm.reset();
pinForm.reset();
}
}, [isDialogOpen, passwordForm, pinForm]);
return ( return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@ -233,7 +237,8 @@ export const Enable2FA = () => {
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Enter your password to enable 2FA Use a custom issuer to identify the service you're
authenticating with.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -250,11 +255,7 @@ export const Enable2FA = () => {
</Form> </Form>
) : ( ) : (
<Form {...pinForm}> <Form {...pinForm}>
<form <form onSubmit={handleVerifySubmit} className="space-y-6">
id="pin-form"
onSubmit={pinForm.handleSubmit(handleVerifySubmit)}
className="space-y-6"
>
<div className="flex flex-col gap-6 justify-center items-center"> <div className="flex flex-col gap-6 justify-center items-center">
{data?.qrCodeUrl ? ( {data?.qrCodeUrl ? (
<> <>
@ -306,36 +307,33 @@ export const Enable2FA = () => {
)} )}
</div> </div>
<FormField <div className="flex flex-col justify-center items-center">
control={pinForm.control} <FormLabel>Verification Code</FormLabel>
name="pin" <InputOTP
render={({ field }) => ( maxLength={6}
<FormItem className="flex flex-col justify-center items-center"> value={otpValue}
<FormLabel>Verification Code</FormLabel> onChange={setOtpValue}
<FormControl> autoComplete="off"
<InputOTP maxLength={6} {...field}> >
<InputOTPGroup> <InputOTPGroup>
<InputOTPSlot index={0} /> <InputOTPSlot index={0} />
<InputOTPSlot index={1} /> <InputOTPSlot index={1} />
<InputOTPSlot index={2} /> <InputOTPSlot index={2} />
<InputOTPSlot index={3} /> <InputOTPSlot index={3} />
<InputOTPSlot index={4} /> <InputOTPSlot index={4} />
<InputOTPSlot index={5} /> <InputOTPSlot index={5} />
</InputOTPGroup> </InputOTPGroup>
</InputOTP> </InputOTP>
</FormControl> <FormDescription>
<FormDescription> Enter the 6-digit code from your authenticator app
Enter the 6-digit code from your authenticator app </FormDescription>
</FormDescription> </div>
<FormMessage />
</FormItem>
)}
/>
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
isLoading={isPasswordLoading} isLoading={isPasswordLoading}
disabled={otpValue.length !== 6}
> >
Enable 2FA Enable 2FA
</Button> </Button>