@@ -16,6 +16,8 @@ export default function AddCodexAccountModal({
1616 const [ error , setError ] = useState ( "" ) ;
1717 const [ authUrl , setAuthUrl ] = useState ( "" ) ;
1818 const [ copied , setCopied ] = useState ( false ) ;
19+ const [ manualCode , setManualCode ] = useState ( "" ) ;
20+ const [ manualCodeBusy , setManualCodeBusy ] = useState ( false ) ;
1921
2022 const aliveRef = useRef ( true ) ;
2123 const pollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
@@ -41,7 +43,13 @@ export default function AddCodexAccountModal({
4143 if ( timeoutRef . current ) { clearTimeout ( timeoutRef . current ) ; timeoutRef . current = null ; }
4244 } , [ ] ) ;
4345
46+ const clearManualCode = useCallback ( ( ) => {
47+ setManualCode ( "" ) ;
48+ setManualCodeBusy ( false ) ;
49+ } , [ ] ) ;
50+
4451 const cancelLogin = useCallback ( async ( ) => {
52+ clearManualCode ( ) ;
4553 const flowId = flowRef . current ;
4654 flowRef . current = null ;
4755 setAuthUrl ( "" ) ;
@@ -54,9 +62,10 @@ export default function AddCodexAccountModal({
5462 headers : { "Content-Type" : "application/json" } ,
5563 body : JSON . stringify ( { flowId } ) ,
5664 } ) . catch ( ( ) => { } ) ;
57- } , [ apiBase , stopPolling ] ) ;
65+ } , [ apiBase , clearManualCode , stopPolling ] ) ;
5866
5967 useEffect ( ( ) => ( ) => {
68+ clearManualCode ( ) ;
6069 aliveRef . current = false ;
6170 loginAbortRef . current ?. abort ( ) ;
6271 loginAbortRef . current = null ;
@@ -72,14 +81,16 @@ export default function AddCodexAccountModal({
7281 body : JSON . stringify ( { flowId } ) ,
7382 } ) . catch ( ( ) => { } ) ;
7483 }
75- } , [ apiBase ] ) ;
84+ } , [ apiBase , clearManualCode ] ) ;
7685
7786 const closeModal = useCallback ( ( ) => {
7887 if ( step === "oauth-waiting" ) void cancelLogin ( ) ;
7988 onCloseRef . current ( ) ;
8089 } , [ step , cancelLogin ] ) ;
8190
8291 const startOAuth = useCallback ( async ( requestedId ?: string ) => {
92+ clearManualCode ( ) ;
93+ flowRef . current = null ;
8394 const controller = new AbortController ( ) ;
8495 loginAbortRef . current ?. abort ( ) ;
8596 loginAbortRef . current = controller ;
@@ -117,12 +128,14 @@ export default function AddCodexAccountModal({
117128 const st = await fetch ( statusUrl ) . then ( r => r . json ( ) ) as { status : string ; error ?: string } ;
118129 if ( st . status === "done" ) {
119130 stopPolling ( ) ;
131+ clearManualCode ( ) ;
120132 flowRef . current = null ;
121133 if ( ! aliveRef . current ) return ;
122134 onAddedRef . current ( ) ;
123135 onCloseRef . current ( ) ;
124136 } else if ( st . status === "error" || st . status === "expired" ) {
125137 stopPolling ( ) ;
138+ clearManualCode ( ) ;
126139 flowRef . current = null ;
127140 if ( aliveRef . current ) {
128141 if ( ! reauthAccountId ) setStep ( "pick" ) ;
@@ -133,6 +146,7 @@ export default function AddCodexAccountModal({
133146 } , 2000 ) ;
134147 timeoutRef . current = setTimeout ( ( ) => {
135148 if ( pollRef . current ) {
149+ clearManualCode ( ) ;
136150 void cancelLogin ( ) ;
137151 if ( aliveRef . current ) {
138152 if ( ! reauthAccountId ) setStep ( "pick" ) ;
@@ -145,7 +159,7 @@ export default function AddCodexAccountModal({
145159 } catch ( e ) {
146160 if ( aliveRef . current && ! ( e instanceof Error && e . name === "AbortError" ) ) setError ( String ( e ) ) ;
147161 }
148- } , [ apiBase , cancelLogin , reauthAccountId , stopPolling , t ] ) ;
162+ } , [ apiBase , cancelLogin , clearManualCode , reauthAccountId , stopPolling , t ] ) ;
149163
150164 useEffect ( ( ) => {
151165 if ( ! reauthAccountId ) {
@@ -179,6 +193,32 @@ export default function AddCodexAccountModal({
179193 }
180194 } ;
181195
196+ const submitManualCode = useCallback ( async ( ) => {
197+ const flowId = flowRef . current ;
198+ const input = manualCode . trim ( ) ;
199+ if ( ! flowId || ! input || manualCodeBusy ) return ;
200+ setManualCodeBusy ( true ) ;
201+ setManualCode ( "" ) ;
202+ try {
203+ const resp = await fetch ( `${ apiBase } /api/codex-auth/login/code` , {
204+ method : "POST" ,
205+ headers : { "Content-Type" : "application/json" } ,
206+ body : JSON . stringify ( { flowId, input } ) ,
207+ } ) ;
208+ const data = await resp . json ( ) . catch ( ( ) => ( { } ) ) as { error ?: string } ;
209+ if ( ! aliveRef . current ) return ;
210+ if ( ! resp . ok ) {
211+ setError ( t ( "prov.pasteFail" , { error : data . error ?? resp . statusText } ) ) ;
212+ return ;
213+ }
214+ setError ( "" ) ;
215+ } catch {
216+ if ( aliveRef . current ) setError ( t ( "modal.networkError" ) ) ;
217+ } finally {
218+ if ( aliveRef . current ) setManualCodeBusy ( false ) ;
219+ }
220+ } , [ apiBase , manualCode , manualCodeBusy , t ] ) ;
221+
182222 // Focus-trap: focus first interactive element on mount, restore on unmount.
183223 useEffect ( ( ) => {
184224 previousFocusRef . current = document . activeElement as HTMLElement | null ;
@@ -244,6 +284,37 @@ export default function AddCodexAccountModal({
244284 < button className = "btn btn-ghost" onClick = { copyLoginLink } disabled = { ! authUrl } style = { { width : "100%" , justifyContent : "center" , marginTop : 12 } } >
245285 < IconLink width = { 14 } /> { copied ? t ( "codexAuth.loginLinkCopied" ) : t ( "codexAuth.copyLoginLink" ) }
246286 </ button >
287+ < div style = { { display : "flex" , flexDirection : "column" , gap : 6 , marginTop : 12 } } >
288+ < div className = "muted text-label" > { t ( "prov.pasteRedirectHint" ) } </ div >
289+ < div style = { { display : "flex" , gap : 8 } } >
290+ < input
291+ type = "text"
292+ autoComplete = "off"
293+ spellCheck = { false }
294+ value = { manualCode }
295+ onChange = { e => setManualCode ( e . target . value ) }
296+ onKeyDown = { e => {
297+ if ( e . key === "Enter" ) {
298+ e . preventDefault ( ) ;
299+ void submitManualCode ( ) ;
300+ }
301+ } }
302+ placeholder = { t ( "prov.pasteRedirect" ) }
303+ aria-label = { t ( "prov.pasteRedirect" ) }
304+ disabled = { manualCodeBusy }
305+ className = "input text-label"
306+ style = { { flex : 1 } }
307+ />
308+ < button
309+ className = "btn btn-ghost"
310+ type = "button"
311+ disabled = { manualCodeBusy || ! manualCode . trim ( ) || ! flowRef . current }
312+ onClick = { ( ) => void submitManualCode ( ) }
313+ >
314+ { manualCodeBusy ? t ( "prov.pasteSubmitting" ) : t ( "prov.pasteSubmit" ) }
315+ </ button >
316+ </ div >
317+ </ div >
247318 { error && < div className = "notice notice-err" style = { { marginTop : 12 } } > { error } </ div > }
248319 < div style = { { textAlign : "center" , padding : "24px 0" } } >
249320 < span className = "spin" style = { { width : 24 , height : 24 } } />
0 commit comments