Skip to content

Commit de02ce2

Browse files
0skiTrigger.dev RepoOps
authored andcommitted
feat(webapp): autofocus, paste and auto-submit on the MFA login code input
The multi-factor authentication login page now focuses the code input on load, accepts pasted codes (including ones with spaces or dashes), and submits as soon as all six digits are entered. After a rejected code the input clears and is focused again for the next attempt. Mono-RevId: b1c662c1a1acba3f8c7e61d08d18c516e1852ab1
1 parent 309ed56 commit de02ce2

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
The multi-factor authentication login page now focuses the code input, accepts pasted codes, and submits as soon as all six digits are entered.

‎apps/webapp/app/routes/login.mfa/route.tsx‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import type {
55
Session,
66
} from "@remix-run/node";
77
import { redirect } from "@remix-run/node";
8-
import { Form, useNavigation } from "@remix-run/react";
8+
import { Form, useNavigation, useSubmit } from "@remix-run/react";
9+
import { REGEXP_ONLY_DIGITS } from "input-otp";
910
import React, { useState } from "react";
1011
import { typedjson, useTypedLoaderData } from "remix-typedjson";
1112
import { z } from "zod";
@@ -181,19 +182,28 @@ export default function LoginMfaPage() {
181182
const data = useTypedLoaderData<typeof loader>();
182183
const rawMfaError = "mfaError" in data ? data.mfaError : undefined;
183184
const navigate = useNavigation();
185+
const submit = useSubmit();
186+
const mfaInputRef = React.useRef<HTMLInputElement>(null);
184187
const [showRecoveryCode, setShowRecoveryCode] = useState(false);
185188
const [mfaCode, setMfaCode] = useState("");
186189
const [hideError, setHideError] = useState(false);
187190

188-
// Clear the MFA code when form submission completes (success or failure)
191+
// Clear and refocus the MFA code once a submission settles. A failed attempt redirects back,
192+
// so the navigation goes submitting -> loading -> idle rather than straight to idle.
189193
const prevNavigationState = React.useRef(navigate.state);
190194
React.useEffect(() => {
191-
if (prevNavigationState.current === "submitting" && navigate.state === "idle") {
195+
if (prevNavigationState.current !== "idle" && navigate.state === "idle") {
192196
setMfaCode("");
197+
mfaInputRef.current?.focus();
193198
}
194199
prevNavigationState.current = navigate.state;
195200
}, [navigate.state]);
196201

202+
const submitMfaCode = (code: string) => {
203+
if (navigate.state !== "idle") return;
204+
submit({ action: "verify-mfa", mfaCode: code }, { method: "post" });
205+
};
206+
197207
// Reset hideError when a new error appears
198208
React.useEffect(() => {
199209
if (rawMfaError) {
@@ -274,14 +284,20 @@ export default function LoginMfaPage() {
274284
</Paragraph>
275285
<Fieldset className="flex w-full flex-col items-center gap-y-2">
276286
<InputOTP
287+
ref={mfaInputRef}
277288
maxLength={6}
278289
value={mfaCode}
279290
onChange={(value) => setMfaCode(value)}
291+
onComplete={submitMfaCode}
292+
pattern={REGEXP_ONLY_DIGITS}
293+
pasteTransformer={(pasted) => pasted.replace(/\D/g, "")}
294+
autoComplete="one-time-code"
295+
autoFocus
280296
variant="large"
281297
fullWidth
282298
>
283299
<InputOTPGroup variant="large" fullWidth>
284-
<InputOTPSlot index={0} autoFocus variant="large" fullWidth />
300+
<InputOTPSlot index={0} variant="large" fullWidth />
285301
<InputOTPSlot index={1} variant="large" fullWidth />
286302
<InputOTPSlot index={2} variant="large" fullWidth />
287303
<InputOTPSlot index={3} variant="large" fullWidth />

0 commit comments

Comments
 (0)