Skip to content

Commit ebed9f6

Browse files
committed
refactor(ui): adopt react-hook-form for remaining auth forms
1 parent f6e3cc5 commit ebed9f6

24 files changed

Lines changed: 1476 additions & 268 deletions

ui/apps/console/src/pages/ForgotPassword.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
1-
import { useState, FormEvent } from "react";
1+
import { useState } from "react";
22
import { Link } from "react-router-dom";
3+
import { useForm } from "react-hook-form";
34
import {
45
EnvelopeIcon,
56
CheckCircleIcon,
67
LockClosedIcon,
78
} from "@heroicons/react/24/outline";
89
import { Button } from "@shellhub/design-system/primitives";
910
import { recoverPassword } from "../client";
10-
import InputField from "@/components/common/fields/InputField";
11+
import FormInputField from "@/components/common/fields/rhf/FormInputField";
12+
import {
13+
forgotPasswordResolver,
14+
type ForgotPasswordFormValues,
15+
} from "./setup/forgotPasswordResolver";
1116

1217
export default function ForgotPassword() {
13-
const [account, setAccount] = useState("");
1418
const [loading, setLoading] = useState(false);
1519
const [sent, setSent] = useState(false);
1620

17-
const handleSubmit = async (e: FormEvent) => {
18-
e.preventDefault();
19-
if (loading) return;
21+
const { control, handleSubmit, formState } = useForm<ForgotPasswordFormValues>({
22+
resolver: forgotPasswordResolver,
23+
mode: "onTouched",
24+
defaultValues: { account: "" },
25+
});
26+
27+
const onSubmit = async (values: ForgotPasswordFormValues) => {
2028
setLoading(true);
2129
try {
2230
await recoverPassword({
23-
body: { username: account.trim() },
31+
body: { username: values.account },
2432
throwOnError: true,
2533
});
2634
} catch {
@@ -83,15 +91,14 @@ export default function ForgotPassword() {
8391
</div>
8492
</div>
8593
) : (
86-
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-5">
87-
<InputField
94+
<form onSubmit={(e) => void handleSubmit(onSubmit)(e)} className="space-y-5">
95+
<FormInputField<ForgotPasswordFormValues>
96+
name="account"
97+
control={control}
8898
id="account"
8999
label="Username or email address"
90-
value={account}
91-
onChange={setAccount}
92100
placeholder="username or email"
93101
autoComplete="username"
94-
95102
required
96103
/>
97104

@@ -102,7 +109,7 @@ export default function ForgotPassword() {
102109
type="submit"
103110
className="px-4"
104111
loading={loading}
105-
disabled={loading || !account.trim()}
112+
disabled={loading || !formState.isValid}
106113
icon={<EnvelopeIcon className="w-4 h-4" strokeWidth={2} />}
107114
>
108115
{loading ? "Sending..." : "Reset Password"}

ui/apps/console/src/pages/Login.tsx

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useEffect, FormEvent } from "react";
2+
import { useForm } from "react-hook-form";
23
import { isSdkError } from "../api/errors";
34
import {
45
useNavigate,
@@ -16,8 +17,12 @@ import { getConfig } from "../env";
1617
import { getSafeRedirect } from "../utils/navigation";
1718
import AuthFooterLinks from "../components/common/AuthFooterLinks";
1819
import { getInfo, getSamlAuthUrl } from "../client";
19-
import InputField from "@/components/common/fields/InputField";
20-
import PasswordField from "@/components/common/fields/PasswordField";
20+
import {
21+
FormInputField,
22+
FormPasswordField,
23+
} from "@/components/common/fields/rhf";
24+
import { loginResolver } from "./setup/loginResolver";
25+
import type { LoginFormValues } from "./setup/loginResolver";
2126

2227
interface CountdownState {
2328
display: string;
@@ -95,18 +100,18 @@ export default function Login() {
95100
.catch(() => setAuthentication(null));
96101
}, []);
97102

98-
const [username, setUsername] = useState("");
99-
const [password, setPassword] = useState("");
100103
const [error, setError] = useState<string | null>(null);
101104
const [lockoutEndEpoch, setLockoutEndEpoch] = useState<number | null>(null);
102105
const { login, loading } = useAuthStore();
103106
const navigate = useNavigate();
104107
const { display: countdownDisplay, expired: lockoutExpired } =
105108
useLoginCountdown(lockoutEndEpoch);
106109

107-
const trimmedUsername = username.trim();
108-
109-
const disableSubmit = trimmedUsername === "" || password === "" || loading;
110+
const { control, handleSubmit, formState } = useForm<LoginFormValues>({
111+
resolver: loginResolver,
112+
mode: "onTouched",
113+
defaultValues: { username: "", password: "" },
114+
});
110115

111116
useEffect(() => {
112117
if (!queryToken) return;
@@ -133,12 +138,11 @@ export default function Login() {
133138
}
134139
};
135140

136-
const handleSubmit = async (e: FormEvent) => {
137-
e.preventDefault();
141+
const onSubmit = async (values: LoginFormValues) => {
138142
setError(null);
139143
setLockoutEndEpoch(null);
140144
try {
141-
await login(trimmedUsername, password);
145+
await login(values.username, values.password);
142146

143147
const state = useAuthStore.getState();
144148
const params = new URLSearchParams(location.search);
@@ -167,7 +171,7 @@ export default function Login() {
167171
break;
168172
case 403:
169173
void navigate(
170-
`/confirm-account?username=${encodeURIComponent(trimmedUsername)}`,
174+
`/confirm-account?username=${encodeURIComponent(values.username)}`,
171175
);
172176
break;
173177
case 429: {
@@ -182,7 +186,10 @@ export default function Login() {
182186
setError("Something went wrong on our end. Please try again later.");
183187
}
184188
}
185-
// Else: error is already set in store
189+
};
190+
191+
const handleFormSubmit = (e: FormEvent) => {
192+
void handleSubmit(onSubmit)(e);
186193
};
187194

188195
// On enterprise, show the local form only once we know local auth is enabled.
@@ -261,25 +268,23 @@ export default function Login() {
261268
className="w-full max-w-sm bg-card/80 border border-border rounded-2xl p-8 backdrop-blur-sm animate-slide-up"
262269
style={{ animationDelay: "200ms" }}
263270
>
264-
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-5">
265-
<InputField
271+
<form onSubmit={handleFormSubmit} className="space-y-5">
272+
<FormInputField<LoginFormValues>
266273
id="username"
267274
label="Username"
268-
value={username}
269-
onChange={setUsername}
275+
name="username"
276+
control={control}
270277
placeholder="username"
271278
autoComplete="username"
272-
required
273279
/>
274280

275-
<PasswordField
281+
<FormPasswordField<LoginFormValues>
276282
id="password"
277283
label="Password"
278-
value={password}
279-
onChange={setPassword}
284+
name="password"
285+
control={control}
280286
placeholder="password"
281287
autoComplete="current-password"
282-
required
283288
/>
284289

285290
{isCloud && (
@@ -300,7 +305,7 @@ export default function Login() {
300305
type="submit"
301306
className="px-4"
302307
loading={loading}
303-
disabled={disableSubmit}
308+
disabled={!formState.isValid || loading}
304309
>
305310
{loading ? "Authenticating..." : "Sign In"}
306311
</Button>

ui/apps/console/src/pages/MfaRecover.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { useState, FormEvent, useEffect } from "react";
22
import { useNavigate, Link } from "react-router-dom";
33
import { KeyIcon } from "@heroicons/react/24/outline";
4+
import { useForm } from "react-hook-form";
45
import { Button, Callout } from "@shellhub/design-system/primitives";
56
import { useAuthStore } from "../stores/authStore";
67
import { disableMfa } from "../client";
78
import MfaRecoveryTimeoutModal from "../components/mfa/MfaRecoveryTimeoutModal";
89
import AuthFooterLinks from "../components/common/AuthFooterLinks";
10+
import { mfaRecoverResolver } from "./setup/mfaRecoverResolver";
11+
import type { MfaRecoverFormValues } from "./setup/mfaRecoverResolver";
912

1013
export default function MfaRecover() {
1114
const {
@@ -21,43 +24,45 @@ export default function MfaRecover() {
2124
const navigate = useNavigate();
2225

2326
const identifier = user || username;
24-
const [recoveryCode, setRecoveryCode] = useState("");
2527
const [showTimeoutModal, setShowTimeoutModal] = useState(false);
2628

27-
// Clear stale error from previous session
29+
const { register, handleSubmit, getValues, resetField, formState } =
30+
useForm<MfaRecoverFormValues>({
31+
resolver: mfaRecoverResolver,
32+
mode: "onTouched",
33+
defaultValues: { recoveryCode: "" },
34+
});
35+
2836
useEffect(() => {
2937
useAuthStore.setState({ error: null });
3038
}, []);
3139

32-
// Redirect to login if no identifier available (but only if not in active MFA session)
3340
useEffect(() => {
3441
if (!identifier && !mfaToken) {
3542
void navigate("/login");
3643
}
3744
}, [identifier, mfaToken, navigate]);
3845

39-
// Don't render if we don't have an identifier
4046
if (!identifier) {
4147
return null;
4248
}
4349

44-
const handleSubmit = async (e: FormEvent) => {
45-
e.preventDefault();
46-
if (!recoveryCode.trim()) return;
47-
50+
const onSubmit = async (values: MfaRecoverFormValues) => {
4851
try {
49-
await recoverWithCode(recoveryCode, identifier);
52+
await recoverWithCode(values.recoveryCode, identifier);
5053
setShowTimeoutModal(true);
5154
} catch {
52-
// Error is set in store
53-
setRecoveryCode("");
55+
resetField("recoveryCode");
5456
}
5557
};
5658

59+
const handleFormSubmit = (e: FormEvent) => {
60+
void handleSubmit(onSubmit)(e);
61+
};
62+
5763
const handleDisableMfa = async () => {
58-
// Use the recovery code that was just entered
5964
await disableMfa({
60-
body: { recovery_code: recoveryCode },
65+
body: { recovery_code: getValues("recoveryCode").trim() },
6166
throwOnError: true,
6267
});
6368
updateMfaStatus(false);
@@ -101,7 +106,7 @@ export default function MfaRecover() {
101106
className="w-full max-w-sm bg-card/80 border border-border rounded-2xl p-8 backdrop-blur-sm animate-slide-up"
102107
style={{ animationDelay: "200ms" }}
103108
>
104-
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-5">
109+
<form onSubmit={handleFormSubmit} className="space-y-5">
105110
{error && <Callout variant="error">{error}</Callout>}
106111

107112
<div>
@@ -114,9 +119,7 @@ export default function MfaRecover() {
114119
<input
115120
id="recovery-code"
116121
type="text"
117-
value={recoveryCode}
118-
onChange={(e) => setRecoveryCode(e.target.value)}
119-
required
122+
{...register("recoveryCode")}
120123
className="w-full px-4 py-3 bg-background border border-border rounded-lg text-sm text-text-primary font-mono placeholder:text-text-secondary focus:outline-none focus:border-accent-yellow/50 focus:ring-1 focus:ring-accent-yellow/20 transition-all duration-200"
121124
placeholder="Enter recovery code"
122125
/>
@@ -130,7 +133,7 @@ export default function MfaRecover() {
130133
fullWidth
131134
type="submit"
132135
loading={loading}
133-
disabled={loading || !recoveryCode.trim()}
136+
disabled={loading || !formState.isValid}
134137
>
135138
{loading ? "Recovering..." : "Recover Account"}
136139
</Button>

ui/apps/console/src/pages/MfaResetRequest.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { FormEvent, useEffect, useState } from "react";
1+
import { useEffect, useState } from "react";
22
import { useNavigate, Link } from "react-router-dom";
3+
import { useForm } from "react-hook-form";
34
import { EnvelopeIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
45
import { Button, Callout } from "@shellhub/design-system/primitives";
56
import { useAuthStore } from "../stores/authStore";
@@ -14,6 +15,8 @@ export default function MfaResetRequest() {
1415

1516
const identifier = user || username;
1617

18+
const { handleSubmit } = useForm();
19+
1720
// Clear stale error from previous session
1821
useEffect(() => {
1922
useMfaResetStore.setState({ error: null });
@@ -31,9 +34,7 @@ export default function MfaResetRequest() {
3134
return null;
3235
}
3336

34-
const handleSubmit = async (e: FormEvent) => {
35-
e.preventDefault();
36-
37+
const onSubmit = async () => {
3738
try {
3839
await requestMfaReset(identifier);
3940
setEmailsSent(true);
@@ -72,7 +73,7 @@ export default function MfaResetRequest() {
7273
style={{ animationDelay: "200ms" }}
7374
>
7475
{!emailsSent ? (
75-
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-5">
76+
<form onSubmit={(e) => void handleSubmit(onSubmit)(e)} className="space-y-5">
7677
{error && <Callout variant="error">{error}</Callout>}
7778

7879
<div className="p-4 bg-primary/5 border border-primary/20 rounded-lg">

0 commit comments

Comments
 (0)