@@ -3,31 +3,38 @@ import { IconGlobe, IconLink } from "../icons";
33import { useT } from "../i18n" ;
44
55export default function AddCodexAccountModal ( {
6- apiBase, onClose, onAdded,
6+ apiBase, onClose, onAdded, reauthAccountId ,
77} : {
88 apiBase : string ;
99 onClose : ( ) => void ;
1010 onAdded : ( ) => void ;
11+ reauthAccountId ?: string ;
1112} ) {
1213 const t = useT ( ) ;
13- const aliveRef = useRef ( true ) ;
14- const pollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
15- const timeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
16- const flowRef = useRef < string | null > ( null ) ;
17- useEffect ( ( ) => ( ) => {
18- aliveRef . current = false ;
19- if ( pollRef . current ) { clearInterval ( pollRef . current ) ; pollRef . current = null ; }
20- if ( timeoutRef . current ) { clearTimeout ( timeoutRef . current ) ; timeoutRef . current = null ; }
21- } , [ ] ) ;
22-
23- const [ step , setStep ] = useState < "pick" | "oauth-waiting" > ( "pick" ) ;
14+ const [ step , setStep ] = useState < "pick" | "oauth-waiting" > ( reauthAccountId ? "oauth-waiting" : "pick" ) ;
2415 const [ id , setId ] = useState ( "" ) ;
2516 const [ error , setError ] = useState ( "" ) ;
2617 const [ authUrl , setAuthUrl ] = useState ( "" ) ;
2718 const [ copied , setCopied ] = useState ( false ) ;
2819
20+ const aliveRef = useRef ( true ) ;
21+ const pollRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
22+ const timeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
23+ const flowRef = useRef < string | null > ( null ) ;
24+ /** Ensures reauth auto-start runs once per account id, even if startOAuth identity changes. */
25+ const startedReauthRef = useRef < string | null > ( null ) ;
26+ const onAddedRef = useRef ( onAdded ) ;
27+ const onCloseRef = useRef ( onClose ) ;
2928 const previousFocusRef = useRef < HTMLElement | null > ( null ) ;
3029 const dialogRef = useRef < HTMLDivElement > ( null ) ;
30+
31+ useEffect ( ( ) => {
32+ onAddedRef . current = onAdded ;
33+ } , [ onAdded ] ) ;
34+ useEffect ( ( ) => {
35+ onCloseRef . current = onClose ;
36+ } , [ onClose ] ) ;
37+
3138 const stopPolling = useCallback ( ( ) => {
3239 if ( pollRef . current ) { clearInterval ( pollRef . current ) ; pollRef . current = null ; }
3340 if ( timeoutRef . current ) { clearTimeout ( timeoutRef . current ) ; timeoutRef . current = null ; }
@@ -46,10 +53,96 @@ export default function AddCodexAccountModal({
4653 } ) . catch ( ( ) => { } ) ;
4754 } , [ apiBase , stopPolling ] ) ;
4855
56+ useEffect ( ( ) => ( ) => {
57+ aliveRef . current = false ;
58+ const flowId = flowRef . current ;
59+ flowRef . current = null ;
60+ if ( pollRef . current ) { clearInterval ( pollRef . current ) ; pollRef . current = null ; }
61+ if ( timeoutRef . current ) { clearTimeout ( timeoutRef . current ) ; timeoutRef . current = null ; }
62+ // Cancel in-flight OAuth so a remounted modal cannot race a stale chatgpt scratch slot.
63+ if ( flowId ) {
64+ void fetch ( `${ apiBase } /api/codex-auth/login/cancel` , {
65+ method : "POST" ,
66+ headers : { "Content-Type" : "application/json" } ,
67+ body : JSON . stringify ( { flowId } ) ,
68+ } ) . catch ( ( ) => { } ) ;
69+ }
70+ } , [ apiBase ] ) ;
71+
4972 const closeModal = useCallback ( ( ) => {
5073 if ( step === "oauth-waiting" ) void cancelLogin ( ) ;
51- onClose ( ) ;
52- } , [ step , onClose , cancelLogin ] ) ;
74+ onCloseRef . current ( ) ;
75+ } , [ step , cancelLogin ] ) ;
76+
77+ const startOAuth = useCallback ( async ( requestedId ?: string ) => {
78+ setError ( "" ) ;
79+ try {
80+ const accountId = reauthAccountId ?? requestedId ?. trim ( ) ?? "" ;
81+ const resp = await fetch ( `${ apiBase } /api/codex-auth/login` , {
82+ method : "POST" ,
83+ headers : { "Content-Type" : "application/json" } ,
84+ body : JSON . stringify (
85+ reauthAccountId
86+ ? { id : reauthAccountId , reauth : true }
87+ : ( accountId ? { id : accountId } : { } ) ,
88+ ) ,
89+ } ) ;
90+ const data = await resp . json ( ) as { url ?: string ; flowId ?: string ; error ?: string ; status ?: string } ;
91+ if ( resp . status === 409 ) {
92+ setError ( t ( "codexAuth.oauthAlreadyInProgress" ) ) ;
93+ return ;
94+ }
95+ if ( data . url ) {
96+ flowRef . current = data . flowId ?? null ;
97+ setAuthUrl ( data . url ) ;
98+ setStep ( "oauth-waiting" ) ;
99+ stopPolling ( ) ;
100+ const fid = data . flowId ?? "" ;
101+ const reauthQuery = reauthAccountId ? "&reauth=1" : "" ;
102+ const statusUrl = fid
103+ ? `${ apiBase } /api/codex-auth/login-status?flowId=${ encodeURIComponent ( fid ) } ${ accountId ? `&accountId=${ encodeURIComponent ( accountId ) } ` : "" } ${ reauthQuery } `
104+ : `${ apiBase } /api/codex-auth/login-status` ;
105+ pollRef . current = setInterval ( async ( ) => {
106+ try {
107+ const st = await fetch ( statusUrl ) . then ( r => r . json ( ) ) as { status : string ; error ?: string } ;
108+ if ( st . status === "done" ) {
109+ stopPolling ( ) ;
110+ flowRef . current = null ;
111+ onAddedRef . current ( ) ;
112+ onCloseRef . current ( ) ;
113+ } else if ( st . status === "error" || st . status === "expired" ) {
114+ stopPolling ( ) ;
115+ flowRef . current = null ;
116+ if ( aliveRef . current ) {
117+ if ( ! reauthAccountId ) setStep ( "pick" ) ;
118+ setError ( st . error ?? "Login failed" ) ;
119+ }
120+ }
121+ } catch { /* ignore network errors during polling */ }
122+ } , 2000 ) ;
123+ timeoutRef . current = setTimeout ( ( ) => {
124+ if ( pollRef . current ) {
125+ void cancelLogin ( ) ;
126+ if ( aliveRef . current ) {
127+ if ( ! reauthAccountId ) setStep ( "pick" ) ;
128+ setError ( t ( "modal.loginTimeout" ) ) ;
129+ }
130+ }
131+ } , 300_000 ) ;
132+ }
133+ if ( data . error && ! data . url ) setError ( data . error ) ;
134+ } catch ( e ) { setError ( String ( e ) ) ; }
135+ } , [ apiBase , cancelLogin , reauthAccountId , stopPolling , t ] ) ;
136+
137+ useEffect ( ( ) => {
138+ if ( ! reauthAccountId ) {
139+ startedReauthRef . current = null ;
140+ return ;
141+ }
142+ if ( startedReauthRef . current === reauthAccountId ) return ;
143+ startedReauthRef . current = reauthAccountId ;
144+ void startOAuth ( ) ;
145+ } , [ reauthAccountId , startOAuth ] ) ;
53146
54147 const copyLoginLink = async ( ) => {
55148 if ( ! authUrl ) return ;
@@ -59,8 +152,8 @@ export default function AddCodexAccountModal({
59152 } else {
60153 const input = document . createElement ( "textarea" ) ;
61154 input . value = authUrl ;
62- input . style . position = "fixed" ;
63155 input . style . opacity = "0" ;
156+ input . style . position = "fixed" ;
64157 document . body . appendChild ( input ) ;
65158 input . select ( ) ;
66159 document . execCommand ( "copy" ) ;
@@ -94,8 +187,10 @@ export default function AddCodexAccountModal({
94187 return ( ) => window . removeEventListener ( "keydown" , onKey ) ;
95188 } , [ closeModal ] ) ;
96189
190+ const dialogLabel = reauthAccountId ? t ( "codexAuth.reauthenticate" ) : t ( "codexAuth.addTitle" ) ;
191+
97192 return (
98- < div role = "dialog" aria-modal = "true" aria-label = { t ( "codexAuth.addTitle" ) } className = "modal-overlay" onClick = { closeModal } >
193+ < div role = "dialog" aria-modal = "true" aria-label = { dialogLabel } className = "modal-overlay" onClick = { closeModal } >
99194 < div ref = { dialogRef } className = "modal-card" onClick = { e => e . stopPropagation ( ) } style = { { maxWidth : 440 } } >
100195 { step === "pick" && (
101196 < >
@@ -111,54 +206,7 @@ export default function AddCodexAccountModal({
111206 style = { { marginBottom : 12 } }
112207 />
113208
114- < button className = "list-row" onClick = { async ( ) => {
115- setError ( "" ) ;
116- try {
117- const requestedId = id . trim ( ) ;
118- const resp = await fetch ( `${ apiBase } /api/codex-auth/login` , {
119- method : "POST" ,
120- headers : { "Content-Type" : "application/json" } ,
121- body : JSON . stringify ( requestedId ? { id : requestedId } : { } ) ,
122- } ) ;
123- const data = await resp . json ( ) as { url ?: string ; flowId ?: string ; error ?: string ; status ?: string } ;
124- if ( resp . status === 409 ) {
125- setError ( t ( "codexAuth.oauthAlreadyInProgress" ) ) ;
126- return ;
127- }
128- if ( data . url ) {
129- flowRef . current = data . flowId ?? null ;
130- setAuthUrl ( data . url ) ;
131- setStep ( "oauth-waiting" ) ;
132- stopPolling ( ) ;
133- const fid = data . flowId ?? "" ;
134- const statusUrl = fid
135- ? `${ apiBase } /api/codex-auth/login-status?flowId=${ encodeURIComponent ( fid ) } ${ requestedId ? `&accountId=${ encodeURIComponent ( requestedId ) } ` : "" } `
136- : `${ apiBase } /api/codex-auth/login-status` ;
137- pollRef . current = setInterval ( async ( ) => {
138- try {
139- const st = await fetch ( statusUrl ) . then ( r => r . json ( ) ) as { status : string ; error ?: string } ;
140- if ( st . status === "done" ) {
141- stopPolling ( ) ;
142- flowRef . current = null ;
143- onAdded ( ) ;
144- onClose ( ) ;
145- } else if ( st . status === "error" || st . status === "expired" ) {
146- stopPolling ( ) ;
147- flowRef . current = null ;
148- if ( aliveRef . current ) { setStep ( "pick" ) ; setError ( st . error ?? "Login failed" ) ; }
149- }
150- } catch { /* ignore network errors during polling */ }
151- } , 2000 ) ;
152- timeoutRef . current = setTimeout ( ( ) => {
153- if ( pollRef . current ) {
154- void cancelLogin ( ) ;
155- if ( aliveRef . current ) { setStep ( "pick" ) ; setError ( t ( "modal.loginTimeout" ) ) ; }
156- }
157- } , 300_000 ) ;
158- }
159- if ( data . error && ! data . url ) setError ( data . error ) ;
160- } catch ( e ) { setError ( String ( e ) ) ; }
161- } } style = { { marginBottom : 8 } } >
209+ < button className = "list-row" onClick = { ( ) => void startOAuth ( id ) } style = { { marginBottom : 8 } } >
162210 < div style = { { display : "flex" , alignItems : "center" , gap : 10 } } >
163211 < IconGlobe width = { 20 } />
164212 < div >
@@ -178,7 +226,7 @@ export default function AddCodexAccountModal({
178226
179227 { step === "oauth-waiting" && (
180228 < >
181- < h3 style = { { marginBottom : 4 } } > { t ( "codexAuth.oauthLogin" ) } </ h3 >
229+ < h3 style = { { marginBottom : 4 } } > { reauthAccountId ? t ( "codexAuth.reauthenticate" ) : t ( "codexAuth.oauthLogin" ) } </ h3 >
182230 < p className = "modal-desc" > { t ( "codexAuth.oauthWaiting" ) } </ p >
183231 < button className = "btn btn-ghost" onClick = { copyLoginLink } disabled = { ! authUrl } style = { { width : "100%" , justifyContent : "center" , marginTop : 12 } } >
184232 < IconLink width = { 14 } /> { copied ? t ( "codexAuth.loginLinkCopied" ) : t ( "codexAuth.copyLoginLink" ) }
0 commit comments