11import { Form } from "@remix-run/react" ;
2+ import { DownloadIcon } from "lucide-react" ;
23import { QRCodeSVG } from "qrcode.react" ;
34import { useState } from "react" ;
45import { Button } from "~/components/primitives/Buttons" ;
56import { CopyableText } from "~/components/primitives/CopyableText" ;
7+ import { CopyButton } from "~/components/primitives/CopyButton" ;
68import {
79 Dialog ,
810 DialogContent ,
@@ -20,19 +22,23 @@ interface MfaSetupDialogProps {
2022 secret : string ;
2123 otpAuthUrl : string ;
2224 } ;
25+ recoveryCodes ?: string [ ] ;
2326 error ?: string ;
2427 isSubmitting : boolean ;
2528 onValidate : ( code : string ) => void ;
2629 onCancel : ( ) => void ;
30+ onSaveRecoveryCodes : ( ) => void ;
2731}
2832
2933export function MfaSetupDialog ( {
3034 isOpen,
3135 setupData,
36+ recoveryCodes,
3237 error,
3338 isSubmitting,
3439 onValidate,
3540 onCancel,
41+ onSaveRecoveryCodes,
3642} : MfaSetupDialogProps ) {
3743 const [ totpCode , setTotpCode ] = useState ( "" ) ;
3844
@@ -47,6 +53,86 @@ export function MfaSetupDialog({
4753 onCancel ( ) ;
4854 } ;
4955
56+ const handleRecoverySubmit = ( e : React . FormEvent ) => {
57+ e . preventDefault ( ) ;
58+ onSaveRecoveryCodes ( ) ;
59+ } ;
60+
61+ const downloadRecoveryCodes = ( ) => {
62+ if ( ! recoveryCodes ) return ;
63+
64+ const content = recoveryCodes . join ( "\n" ) ;
65+ const blob = new Blob ( [ content ] , { type : "text/plain" } ) ;
66+ const url = URL . createObjectURL ( blob ) ;
67+ const a = document . createElement ( "a" ) ;
68+ a . href = url ;
69+ a . download = "trigger-dev-recovery-codes.txt" ;
70+ document . body . appendChild ( a ) ;
71+ a . click ( ) ;
72+ document . body . removeChild ( a ) ;
73+ URL . revokeObjectURL ( url ) ;
74+ } ;
75+
76+ // Show recovery codes if they exist
77+ if ( recoveryCodes && recoveryCodes . length > 0 ) {
78+ return (
79+ < Dialog open = { isOpen } >
80+ < DialogContent showCloseButton = { false } >
81+ < DialogHeader >
82+ < DialogTitle > Recovery codes</ DialogTitle >
83+ </ DialogHeader >
84+ < Form method = "post" onSubmit = { handleRecoverySubmit } >
85+ < div className = "flex flex-col gap-2 pb-0 pt-3" >
86+ < Paragraph spacing >
87+ Copy and store these recovery codes carefully in case you lose your device.
88+ </ Paragraph >
89+
90+ < div className = "flex flex-col gap-6 rounded border border-grid-dimmed bg-background-bright pt-6" >
91+ < div className = "grid grid-cols-3 gap-2" >
92+ { recoveryCodes . map ( ( code , index ) => (
93+ < div key = { index } className = "text-center font-mono text-sm text-text-bright" >
94+ { code }
95+ </ div >
96+ ) ) }
97+ </ div >
98+ < div className = "flex items-center justify-end border-t border-grid-bright px-1.5 py-1.5" >
99+ < Button
100+ type = "button"
101+ variant = "minimal/medium"
102+ onClick = { downloadRecoveryCodes }
103+ LeadingIcon = { DownloadIcon }
104+ >
105+ Download
106+ </ Button >
107+ < CopyButton
108+ value = { recoveryCodes . join ( "\n" ) }
109+ buttonVariant = "minimal"
110+ showTooltip = { false }
111+ >
112+ Copy
113+ </ CopyButton >
114+ </ div >
115+ </ div >
116+ </ div >
117+
118+ < DialogFooter className = "justify-end border-t-0" >
119+ < Button
120+ type = "submit"
121+ variant = "primary/medium"
122+ shortcut = { { key : "Enter" } }
123+ hideShortcutKey
124+ autoFocus
125+ >
126+ Continue
127+ </ Button >
128+ </ DialogFooter >
129+ </ Form >
130+ </ DialogContent >
131+ </ Dialog >
132+ ) ;
133+ }
134+
135+ // Show QR setup if no recovery codes yet
50136 if ( ! setupData ) return null ;
51137
52138 return (
0 commit comments