@@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
55import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode" ;
66import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef" ;
77import { useLocale } from "@calcom/lib/hooks/useLocale" ;
8- import { Button , Dialog , DialogContent , DialogFooter , Form , TextField } from "@calcom/ui" ;
8+ import { Button , Dialog , DialogContent , DialogFooter , Form , PasswordField , showToast } from "@calcom/ui" ;
99
1010import TwoFactor from "@components/auth/TwoFactor" ;
1111
@@ -28,6 +28,7 @@ interface EnableTwoFactorModalProps {
2828
2929enum SetupStep {
3030 ConfirmPassword ,
31+ DisplayBackupCodes ,
3132 DisplayQrCode ,
3233 EnterTotpCode ,
3334}
@@ -54,16 +55,25 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
5455
5556 const setupDescriptions = {
5657 [ SetupStep . ConfirmPassword ] : t ( "2fa_confirm_current_password" ) ,
58+ [ SetupStep . DisplayBackupCodes ] : t ( "backup_code_instructions" ) ,
5759 [ SetupStep . DisplayQrCode ] : t ( "2fa_scan_image_or_use_code" ) ,
5860 [ SetupStep . EnterTotpCode ] : t ( "2fa_enter_six_digit_code" ) ,
5961 } ;
6062 const [ step , setStep ] = useState ( SetupStep . ConfirmPassword ) ;
6163 const [ password , setPassword ] = useState ( "" ) ;
64+ const [ backupCodes , setBackupCodes ] = useState ( [ ] ) ;
65+ const [ backupCodesUrl , setBackupCodesUrl ] = useState ( "" ) ;
6266 const [ dataUri , setDataUri ] = useState ( "" ) ;
6367 const [ secret , setSecret ] = useState ( "" ) ;
6468 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
6569 const [ errorMessage , setErrorMessage ] = useState < string | null > ( null ) ;
6670
71+ const resetState = ( ) => {
72+ setPassword ( "" ) ;
73+ setErrorMessage ( null ) ;
74+ setStep ( SetupStep . ConfirmPassword ) ;
75+ } ;
76+
6777 async function handleSetup ( e : React . FormEvent ) {
6878 e . preventDefault ( ) ;
6979
@@ -79,6 +89,15 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
7989 const body = await response . json ( ) ;
8090
8191 if ( response . status === 200 ) {
92+ setBackupCodes ( body . backupCodes ) ;
93+
94+ // create backup codes download url
95+ const textBlob = new Blob ( [ body . backupCodes . map ( formatBackupCode ) . join ( "\n" ) ] , {
96+ type : "text/plain" ,
97+ } ) ;
98+ if ( backupCodesUrl ) URL . revokeObjectURL ( backupCodesUrl ) ;
99+ setBackupCodesUrl ( URL . createObjectURL ( textBlob ) ) ;
100+
82101 setDataUri ( body . dataUri ) ;
83102 setSecret ( body . secret ) ;
84103 setStep ( SetupStep . DisplayQrCode ) ;
@@ -113,7 +132,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
113132 const body = await response . json ( ) ;
114133
115134 if ( response . status === 200 ) {
116- onEnable ( ) ;
135+ setStep ( SetupStep . DisplayBackupCodes ) ;
117136 return ;
118137 }
119138
@@ -141,13 +160,18 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
141160 }
142161 } , [ form , handleEnableRef , totpCode ] ) ;
143162
163+ const formatBackupCode = ( code : string ) => `${ code . slice ( 0 , 5 ) } -${ code . slice ( 5 , 10 ) } ` ;
164+
144165 return (
145166 < Dialog open = { open } onOpenChange = { onOpenChange } >
146- < DialogContent title = { t ( "enable_2fa" ) } description = { setupDescriptions [ step ] } type = "creation" >
167+ < DialogContent
168+ title = { step === SetupStep . DisplayBackupCodes ? t ( "backup_codes" ) : t ( "enable_2fa" ) }
169+ description = { setupDescriptions [ step ] }
170+ type = "creation" >
147171 < WithStep step = { SetupStep . ConfirmPassword } current = { step } >
148172 < form onSubmit = { handleSetup } >
149173 < div className = "mb-4" >
150- < TextField
174+ < PasswordField
151175 label = { t ( "password" ) }
152176 type = "password"
153177 name = "password"
@@ -173,6 +197,15 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
173197 </ p >
174198 </ >
175199 </ WithStep >
200+ < WithStep step = { SetupStep . DisplayBackupCodes } current = { step } >
201+ < >
202+ < div className = "mt-5 grid grid-cols-2 gap-1 text-center font-mono md:pl-10 md:pr-10" >
203+ { backupCodes . map ( ( code ) => (
204+ < div key = { code } > { formatBackupCode ( code ) } </ div >
205+ ) ) }
206+ </ div >
207+ </ >
208+ </ WithStep >
176209 < Form handleSubmit = { handleEnable } form = { form } >
177210 < WithStep step = { SetupStep . EnterTotpCode } current = { step } >
178211 < div className = "-mt-4 pb-2" >
@@ -186,9 +219,16 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
186219 </ div >
187220 </ WithStep >
188221 < DialogFooter className = "mt-8" showDivider >
189- < Button color = "secondary" onClick = { onCancel } >
190- { t ( "cancel" ) }
191- </ Button >
222+ { step !== SetupStep . DisplayBackupCodes ? (
223+ < Button
224+ color = "secondary"
225+ onClick = { ( ) => {
226+ onCancel ( ) ;
227+ resetState ( ) ;
228+ } } >
229+ { t ( "cancel" ) }
230+ </ Button >
231+ ) : null }
192232 < WithStep step = { SetupStep . ConfirmPassword } current = { step } >
193233 < Button
194234 type = "submit"
@@ -218,6 +258,35 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
218258 { t ( "enable" ) }
219259 </ Button >
220260 </ WithStep >
261+ < WithStep step = { SetupStep . DisplayBackupCodes } current = { step } >
262+ < >
263+ < Button
264+ color = "secondary"
265+ data-testid = "backup-codes-close"
266+ onClick = { ( e ) => {
267+ e . preventDefault ( ) ;
268+ resetState ( ) ;
269+ onEnable ( ) ;
270+ } } >
271+ { t ( "close" ) }
272+ </ Button >
273+ < Button
274+ color = "secondary"
275+ data-testid = "backup-codes-copy"
276+ onClick = { ( e ) => {
277+ e . preventDefault ( ) ;
278+ navigator . clipboard . writeText ( backupCodes . map ( formatBackupCode ) . join ( "\n" ) ) ;
279+ showToast ( t ( "backup_codes_copied" ) , "success" ) ;
280+ } } >
281+ { t ( "copy" ) }
282+ </ Button >
283+ < a download = "cal-backup-codes.txt" href = { backupCodesUrl } >
284+ < Button color = "primary" data-testid = "backup-codes-download" >
285+ { t ( "download" ) }
286+ </ Button >
287+ </ a >
288+ </ >
289+ </ WithStep >
221290 </ DialogFooter >
222291 </ Form >
223292 </ DialogContent >
0 commit comments