@@ -35,6 +35,7 @@ import { requireUserId } from "~/services/session.server";
3535import { accountPath } from "~/utils/pathBuilder" ;
3636import { CopyButton } from "~/components/primitives/CopyButton" ;
3737import { DownloadIcon } from "lucide-react" ;
38+ import { InputOTP , InputOTPGroup , InputOTPSlot } from "~/components/primitives/InputOTP" ;
3839
3940export 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