@@ -23,6 +23,12 @@ import { getOryApi } from '../../tools/ory';
2323
2424export interface RecoverAccountModalProps {
2525 email ?: string ;
26+ /**
27+ * Optional pre-existing Kratos recovery flow ID. When provided (e.g. via the `/signin?recover=1&flow=...` deep-link
28+ * Kratos uses for admin-issued recovery codes), the modal jumps straight to the "enter recovery code" step instead of
29+ * asking the user to request a new code.
30+ */
31+ flowId ?: string ;
2632 onClose : ( ) => void ;
2733}
2834
@@ -39,14 +45,38 @@ async function getRecoverFlow(api: FrontendApi, flowId?: string) {
3945 return await api . createBrowserRecoveryFlow ( ) ;
4046}
4147
42- export function RecoverAccountModal ( { email, onClose } : RecoverAccountModalProps ) {
48+ export function RecoverAccountModal ( { email, flowId , onClose } : RecoverAccountModalProps ) {
4349 const { addToast, uiState, refreshUiState } = useAppContext ( ) ;
4450 const navigate = useNavigate ( ) ;
4551
4652 const [ userEmail , setUserEmail ] = useState < string > ( email ?? '' ) ;
4753 const [ recoveryCode , setRecoveryCode ] = useState < string > ( '' ) ;
4854
4955 const [ accountRecoveryStatus , setAccountRecoveryStatus ] = useState < AsyncData < undefined , RecoveryFlow > | null > ( null ) ;
56+
57+ // When a Kratos recovery flow ID is supplied via deep-link, hydrate it on mount so the
58+ // submit step is wired up to the same flow Kratos already issued the code for.
59+ useEffect ( ( ) => {
60+ if ( ! flowId ) {
61+ return ;
62+ }
63+
64+ let cancelled = false ;
65+ getOryApi ( )
66+ . then ( async ( api ) => {
67+ const flow = await getRecoverFlow ( api , flowId ) ;
68+ if ( cancelled ) {
69+ return ;
70+ }
71+ setAccountRecoveryStatus ( { status : 'succeeded' , data : undefined , state : flow } ) ;
72+ } )
73+ . catch ( ( err : unknown ) => {
74+ console . error ( 'Failed to load recovery flow from deep-link.' , err ) ;
75+ } ) ;
76+ return ( ) => {
77+ cancelled = true ;
78+ } ;
79+ } , [ flowId ] ) ;
5080 const onSendRecoveryCode : MouseEventHandler < HTMLButtonElement > = ( e ) => {
5181 e . preventDefault ( ) ;
5282
@@ -156,6 +186,10 @@ export function RecoverAccountModal({ email, onClose }: RecoverAccountModalProps
156186 } , [ uiState , navigate ] ) ;
157187
158188 const awaitingRecoveryCode = ! ! accountRecoveryStatus ?. state ;
189+ // True only when the modal was opened by following a Kratos-issued recovery link and the referenced flow has been
190+ // successfully hydrated. In this mode the email step is bypassed because the flow is already bound to a specific
191+ // identity server-side.
192+ const isDeepLinkMode = ! ! flowId && accountRecoveryStatus ?. state ?. id === flowId ;
159193 return (
160194 < EuiModal onClose = { onClose } >
161195 < EuiModalHeader >
@@ -167,23 +201,32 @@ export function RecoverAccountModal({ email, onClose }: RecoverAccountModalProps
167201 </ EuiModalHeader >
168202 < EuiModalBody >
169203 < EuiForm id = "account-recovery-form" component = "form" >
170- < EuiFormRow label = "Email" >
171- < EuiFieldText
172- value = { userEmail }
173- autoComplete = { 'email' }
174- type = { 'email' }
175- required
176- disabled = { awaitingRecoveryCode }
177- onChange = { ( e ) => setUserEmail ( e . target . value ) }
178- />
179- </ EuiFormRow >
204+ { isDeepLinkMode ? null : (
205+ < EuiFormRow label = "Email" >
206+ < EuiFieldText
207+ value = { userEmail }
208+ autoComplete = { 'email' }
209+ type = { 'email' }
210+ required
211+ disabled = { awaitingRecoveryCode }
212+ onChange = { ( e ) => setUserEmail ( e . target . value ) }
213+ />
214+ </ EuiFormRow >
215+ ) }
180216 { awaitingRecoveryCode ? (
181- < EuiFormRow label = { 'Recovery code' } >
217+ < EuiFormRow
218+ label = { 'Recovery code' }
219+ helpText = { isDeepLinkMode ? 'Enter the code that was issued for your account.' : undefined }
220+ >
182221 < EuiFieldText
183222 value = { recoveryCode }
184223 autoComplete = { 'off' }
185224 type = { 'text' }
186- append = { < EuiButtonIcon iconType = "refresh" onClick = { onSendRecoveryCode } aria-label = "Resend code" /> }
225+ append = {
226+ isDeepLinkMode ? undefined : (
227+ < EuiButtonIcon iconType = "refresh" onClick = { onSendRecoveryCode } aria-label = "Resend code" />
228+ )
229+ }
187230 onChange = { ( e ) => setRecoveryCode ( e . target . value ) }
188231 />
189232 </ EuiFormRow >
@@ -199,7 +242,7 @@ export function RecoverAccountModal({ email, onClose }: RecoverAccountModalProps
199242 type = "submit"
200243 form = "account-recovery-form"
201244 fill
202- disabled = { accountRecoveryStatus ?. status === 'pending' || ! userEmail ?. trim ( ) || ! recoveryCode ?. trim ( ) }
245+ disabled = { accountRecoveryStatus ?. status === 'pending' || ! recoveryCode ?. trim ( ) }
203246 onClick = { onRecoverAccount }
204247 isLoading = { accountRecoveryStatus ?. status === 'pending' }
205248 >
0 commit comments