Skip to content

Commit 3c067b3

Browse files
samejrericallam
authored andcommitted
Improvements to form handling
1 parent c19a899 commit 3c067b3

1 file changed

Lines changed: 105 additions & 76 deletions

File tree

  • apps/webapp/app/routes/account.security

apps/webapp/app/routes/account.security/route.tsx

Lines changed: 105 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { requireUserId } from "~/services/session.server";
3535
import { accountPath } from "~/utils/pathBuilder";
3636
import { CopyButton } from "~/components/primitives/CopyButton";
3737
import { DownloadIcon } from "lucide-react";
38+
import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/primitives/InputOTP";
3839

3940
export const meta: MetaFunction = () => {
4041
return [
@@ -172,7 +173,8 @@ export default function Page() {
172173
}
173174
};
174175

175-
const handleQrConfirm = () => {
176+
const handleQrConfirm = (e: React.FormEvent) => {
177+
e.preventDefault();
176178
// TODO: Submit TOTP code to backend for validation
177179
console.log("Validating TOTP code:", totpCode);
178180

@@ -188,7 +190,8 @@ export default function Page() {
188190
// Don't change the switch state when canceling
189191
};
190192

191-
const handleRecoveryComplete = () => {
193+
const handleRecoveryComplete = (e: React.FormEvent) => {
194+
e.preventDefault();
192195
setShowRecoveryDialog(false);
193196
setIsMfaEnabled(true);
194197
};
@@ -218,7 +221,7 @@ export default function Page() {
218221
</NavBar>
219222

220223
<PageBody>
221-
<MainHorizontallyCenteredContainer className="grid place-items-center">
224+
<MainHorizontallyCenteredContainer className="grid place-items-center overflow-visible">
222225
<div className="mb-3 w-full border-b border-grid-dimmed pb-3">
223226
<Header2>Security</Header2>
224227
</div>
@@ -234,9 +237,9 @@ export default function Page() {
234237
<Switch
235238
id="mfa"
236239
variant="medium"
237-
label="Enable"
240+
label={isMfaEnabled ? "Enabled" : "Enable"}
238241
labelPosition="right"
239-
className="w-fit pr-3"
242+
className="-ml-2 w-fit pr-3"
240243
checked={isMfaEnabled}
241244
onCheckedChange={handleSwitchChange}
242245
/>
@@ -254,46 +257,62 @@ export default function Page() {
254257
<DialogHeader>
255258
<DialogTitle>Enable authenticator app</DialogTitle>
256259
</DialogHeader>
257-
<div className="flex flex-col gap-4 pt-3">
258-
<Paragraph>
259-
Scan the QR code below with your preferred authenticator app then enter the 6
260-
digit code that the app generates. Alternatively, you can copy the secret below
261-
and paste it into your app.
262-
</Paragraph>
263-
264-
<div className="flex flex-col items-center justify-center gap-y-4 rounded border border-grid-dimmed bg-background-bright py-4">
265-
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
266-
<QRCodeSVG value={qrCodeValue} size={300} marginSize={3} />
260+
<Form method="post" onSubmit={handleQrConfirm}>
261+
<input type="hidden" name="action" value="enable-mfa" />
262+
<div className="flex flex-col gap-4 pt-3">
263+
<Paragraph>
264+
Scan the QR code below with your preferred authenticator app then enter the 6
265+
digit code that the app generates. Alternatively, you can copy the secret below
266+
and paste it into your app.
267+
</Paragraph>
268+
269+
<div className="flex flex-col items-center justify-center gap-y-4 rounded border border-grid-dimmed bg-background-bright py-4">
270+
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
271+
<QRCodeSVG value={qrCodeValue} size={300} marginSize={3} />
272+
</div>
273+
<CopyableText value={secretKey} className="font-mono text-base tracking-wide" />
274+
</div>
275+
276+
<div className="mb-4 flex items-center justify-center">
277+
<InputOTP
278+
maxLength={6}
279+
value={totpCode}
280+
onChange={(value) => setTotpCode(value)}
281+
variant="large"
282+
name="totpCode"
283+
onKeyDown={(e) => {
284+
if (e.key === "Enter" && totpCode.length === 6) {
285+
handleQrConfirm(e);
286+
}
287+
}}
288+
>
289+
<InputOTPGroup variant="large">
290+
<InputOTPSlot index={0} variant="large" autoFocus />
291+
<InputOTPSlot index={1} variant="large" />
292+
<InputOTPSlot index={2} variant="large" />
293+
<InputOTPSlot index={3} variant="large" />
294+
<InputOTPSlot index={4} variant="large" />
295+
<InputOTPSlot index={5} variant="large" />
296+
</InputOTPGroup>
297+
</InputOTP>
267298
</div>
268-
<CopyableText value={secretKey} className="font-mono text-sm" />
269299
</div>
270300

271-
<Input
272-
type="text"
273-
variant="large"
274-
value={totpCode}
275-
onChange={(e) => {
276-
const value = e.target.value.replace(/\D/g, "").slice(0, 6);
277-
setTotpCode(value);
278-
}}
279-
placeholder="000000"
280-
maxLength={6}
281-
className="text-center font-mono tracking-wider"
282-
/>
283-
</div>
284-
285-
<DialogFooter>
286-
<Button variant="secondary/medium" onClick={handleQrCancel}>
287-
Cancel
288-
</Button>
289-
<Button
290-
variant="primary/medium"
291-
onClick={handleQrConfirm}
292-
disabled={totpCode.length !== 6}
293-
>
294-
Confirm
295-
</Button>
296-
</DialogFooter>
301+
<DialogFooter>
302+
<Button type="button" variant="secondary/medium" onClick={handleQrCancel}>
303+
Cancel
304+
</Button>
305+
<Button
306+
type="submit"
307+
variant="primary/medium"
308+
disabled={totpCode.length !== 6}
309+
shortcut={{ key: "Enter" }}
310+
hideShortcutKey
311+
>
312+
Confirm
313+
</Button>
314+
</DialogFooter>
315+
</Form>
297316
</DialogContent>
298317
</Dialog>
299318

@@ -303,43 +322,53 @@ export default function Page() {
303322
<DialogHeader>
304323
<DialogTitle>Recovery codes</DialogTitle>
305324
</DialogHeader>
306-
<div className="flex flex-col gap-2 pb-0 pt-3">
307-
<Paragraph spacing>
308-
Copy and store these recovery codes carefully in case you lose your device.
309-
</Paragraph>
310-
311-
<div className="flex flex-col gap-6 rounded border border-grid-dimmed bg-background-bright pt-6">
312-
<div className="grid grid-cols-3 gap-2">
313-
{recoveryCodes.map((code, index) => (
314-
<div key={index} className="text-center font-mono text-sm text-text-bright">
315-
{code}
316-
</div>
317-
))}
318-
</div>
319-
<div className="flex items-center justify-end border-t border-grid-bright py-1.5 pr-1.5">
320-
<Button
321-
variant="minimal/medium"
322-
onClick={downloadRecoveryCodes}
323-
LeadingIcon={DownloadIcon}
324-
>
325-
Download
326-
</Button>
327-
<CopyButton
328-
value={recoveryCodes.join("\n")}
329-
buttonVariant="minimal"
330-
showTooltip={false}
331-
>
332-
Copy
333-
</CopyButton>
325+
<Form method="post" onSubmit={handleRecoveryComplete}>
326+
<input type="hidden" name="action" value="save-recovery-codes" />
327+
<div className="flex flex-col gap-2 pb-0 pt-3">
328+
<Paragraph spacing>
329+
Copy and store these recovery codes carefully in case you lose your device.
330+
</Paragraph>
331+
332+
<div className="flex flex-col gap-6 rounded border border-grid-dimmed bg-background-bright pt-6">
333+
<div className="grid grid-cols-3 gap-2">
334+
{recoveryCodes.map((code, index) => (
335+
<div key={index} className="text-center font-mono text-sm text-text-bright">
336+
{code}
337+
</div>
338+
))}
339+
</div>
340+
<div className="flex items-center justify-end border-t border-grid-bright px-1.5 py-1.5">
341+
<Button
342+
type="button"
343+
variant="minimal/medium"
344+
onClick={downloadRecoveryCodes}
345+
LeadingIcon={DownloadIcon}
346+
>
347+
Download
348+
</Button>
349+
<CopyButton
350+
value={recoveryCodes.join("\n")}
351+
buttonVariant="minimal"
352+
showTooltip={false}
353+
>
354+
Copy
355+
</CopyButton>
356+
</div>
334357
</div>
335358
</div>
336-
</div>
337359

338-
<DialogFooter className="justify-end">
339-
<Button variant="primary/medium" onClick={handleRecoveryComplete}>
340-
Continue
341-
</Button>
342-
</DialogFooter>
360+
<DialogFooter className="justify-end border-t-0">
361+
<Button
362+
type="submit"
363+
variant="primary/medium"
364+
shortcut={{ key: "Enter" }}
365+
hideShortcutKey
366+
autoFocus
367+
>
368+
Continue
369+
</Button>
370+
</DialogFooter>
371+
</Form>
343372
</DialogContent>
344373
</Dialog>
345374
</MainHorizontallyCenteredContainer>

0 commit comments

Comments
 (0)