@@ -32,6 +32,8 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
3232 const [ resetPopup , setResetPopup ] = useState < AccountEntry | null > ( null ) ;
3333 const [ resetConfirm , setResetConfirm ] = useState ( false ) ;
3434 const [ redeeming , setRedeeming ] = useState ( false ) ;
35+ const [ creditDetails , setCreditDetails ] = useState < { granted_at : string ; expires_at : string } [ ] | null > ( null ) ;
36+ const [ creditDetailsLoading , setCreditDetailsLoading ] = useState ( false ) ;
3537
3638 const load = async ( refreshQuota = false ) => {
3739 try {
@@ -87,6 +89,24 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
8789 }
8890 } ;
8991
92+ const openResetPopup = async ( account : AccountEntry ) => {
93+ setResetPopup ( account ) ;
94+ setResetConfirm ( false ) ;
95+ setCreditDetails ( null ) ;
96+ setCreditDetailsLoading ( true ) ;
97+ try {
98+ const resp = await fetch ( `${ apiBase } /api/codex-auth/reset-credits?accountId=${ encodeURIComponent ( account . id ) } ` ) ;
99+ if ( resp . ok ) {
100+ const data = ( await resp . json ( ) ) as { credits ?: { granted_at : string ; expires_at : string } [ ] } ;
101+ const sorted = ( data . credits ?? [ ] ) . sort ( ( a , b ) =>
102+ new Date ( a . granted_at ) . getTime ( ) - new Date ( b . granted_at ) . getTime ( )
103+ ) ;
104+ setCreditDetails ( sorted ) ;
105+ }
106+ } catch { /* detail fetch is non-blocking */ }
107+ finally { setCreditDetailsLoading ( false ) ; }
108+ } ;
109+
90110 const handleRedeem = async ( accountId : string ) => {
91111 setRedeeming ( true ) ;
92112 try {
@@ -139,7 +159,7 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
139159 < div className = "card-head" >
140160 < span className = "dot dot-green" />
141161 < strong > { t ( "codexAuth.mainAccount" ) } </ strong >
142- { main && < TicketBadge account = { { ...main , id : "__main__" } as AccountEntry } onClick = { ( ) => ! isWorkspaceAccount ( main ?. plan ) && setResetPopup ( { ...main , id : "__main__" } as AccountEntry ) } /> }
162+ { main && < TicketBadge account = { { ...main , id : "__main__" } as AccountEntry } onClick = { ( ) => ! isWorkspaceAccount ( main ?. plan ) && openResetPopup ( { ...main , id : "__main__" } as AccountEntry ) } /> }
143163 < span className = { `badge ${ ! activeId ? "badge-primary" : "badge-muted" } ` } >
144164 { ! activeId ? t ( "codexAuth.nextSession" ) : t ( "codexAuth.current" ) }
145165 </ span >
@@ -166,7 +186,7 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
166186 < span className = { `dot ${ a . needsReauth ? "dot-amber" : isNext ( a . id ) ? "dot-blue" : "dot-muted" } ` } />
167187 < strong > { a . email } </ strong >
168188 { a . plan && < span className = "badge badge-green" > { a . plan } </ span > }
169- < TicketBadge account = { a } onClick = { ( ) => ! isWorkspaceAccount ( a . plan ) && setResetPopup ( a ) } />
189+ < TicketBadge account = { a } onClick = { ( ) => ! isWorkspaceAccount ( a . plan ) && openResetPopup ( a ) } />
170190 { a . needsReauth && < span className = "badge badge-amber" > { t ( "codexAuth.needsReauth" ) } </ span > }
171191 { isNext ( a . id ) && ! a . needsReauth && < span className = "badge badge-primary" > { t ( "codexAuth.nextSession" ) } </ span > }
172192 < button
@@ -218,7 +238,7 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
218238 ) }
219239
220240 { resetPopup && (
221- < div className = "modal-overlay" onClick = { ( ) => { setResetPopup ( null ) ; setResetConfirm ( false ) ; } } >
241+ < div className = "modal-overlay" onClick = { ( ) => { setResetPopup ( null ) ; setResetConfirm ( false ) ; setCreditDetails ( null ) ; } } >
222242 < div className = "modal-card" onClick = { e => e . stopPropagation ( ) } >
223243 { ! resetConfirm ? (
224244 < >
@@ -227,12 +247,20 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
227247 < div style = { { margin : "16px 0" } } >
228248 { ( resetPopup . quota ?. resetCredits ?? 0 ) > 0 ? (
229249 < >
230- < p > { t ( "codexAuth.resetCreditsAvailable" , { count : String ( resetPopup . quota ?. resetCredits ?? 0 ) } ) } </ p >
231- < p className = "modal-desc" > { t ( "codexAuth.resetCreditsDesc" ) } </ p >
250+ < p style = { { marginBottom : 12 } } > { t ( "codexAuth.resetCreditsAvailable" , { count : String ( resetPopup . quota ?. resetCredits ?? 0 ) } ) } </ p >
251+ { creditDetailsLoading && < p className = "faint" style = { { fontSize : 12 } } > Loading...</ p > }
252+ { creditDetails && creditDetails . length > 0 && (
253+ < div className = "credit-list" >
254+ { creditDetails . map ( ( c , i ) => (
255+ < CreditItem key = { i } index = { i } grantedAt = { c . granted_at } expiresAt = { c . expires_at } isNext = { i === 0 } t = { t } />
256+ ) ) }
257+ </ div >
258+ ) }
232259 < button className = "btn btn-primary" style = { { marginTop : 12 , width : "100%" } }
233260 onClick = { ( ) => setResetConfirm ( true ) } disabled = { redeeming } >
234261 { t ( "codexAuth.useOneCredit" ) }
235262 </ button >
263+ < p className = "card-sub" style = { { fontSize : 11 , marginTop : 8 , textAlign : "center" } } > { t ( "codexAuth.fifoNote" ) } </ p >
236264 </ >
237265 ) : (
238266 < >
@@ -241,14 +269,18 @@ export default function CodexAuth({ apiBase }: { apiBase: string }) {
241269 </ >
242270 ) }
243271 </ div >
244- < p className = "card-sub" style = { { fontSize : 11 } } > { t ( "codexAuth.creditsExpireNote" ) } </ p >
245272 </ >
246273 ) : (
247274 < >
248275 < div style = { { textAlign : "center" , padding : "12px 0" } } >
249276 < div className = "confirm-icon" > < IconAlert width = { 22 } /> </ div >
250277 < h3 > { t ( "codexAuth.confirmResetTitle" ) } </ h3 >
251278 < p className = "modal-desc" > { t ( "codexAuth.confirmResetDesc" , { count : String ( resetPopup . quota ?. resetCredits ?? 0 ) } ) } </ p >
279+ { creditDetails && creditDetails [ 0 ] && (
280+ < p className = "faint" style = { { fontSize : 12 } } >
281+ { t ( "codexAuth.confirmWhichCredit" , { date : formatCreditDate ( creditDetails [ 0 ] . granted_at ) } ) }
282+ </ p >
283+ ) }
252284 < p className = "faint" style = { { fontSize : 12 } } > { t ( "codexAuth.irreversible" ) } </ p >
253285 </ div >
254286 < div className = "modal-actions" >
@@ -328,6 +360,37 @@ function QuotaRow({ label, percent, resetAt, threshold, t }: { label: string; pe
328360 ) ;
329361}
330362
363+ function formatCreditDate ( iso : string ) : string {
364+ const d = new Date ( iso ) ;
365+ return new Intl . DateTimeFormat ( undefined , { month : "short" , day : "numeric" , year : "numeric" } ) . format ( d ) ;
366+ }
367+
368+ function daysUntil ( iso : string ) : number {
369+ return Math . max ( 0 , Math . ceil ( ( new Date ( iso ) . getTime ( ) - Date . now ( ) ) / 86_400_000 ) ) ;
370+ }
371+
372+ function CreditItem ( { index, grantedAt, expiresAt, isNext, t } : {
373+ index : number ; grantedAt : string ; expiresAt : string ; isNext : boolean ; t : TFn ;
374+ } ) {
375+ const days = daysUntil ( expiresAt ) ;
376+ const urgent = days <= 7 ;
377+ return (
378+ < div className = { `credit-item${ isNext ? " credit-next" : "" } ` } >
379+ < div className = "credit-item-head" >
380+ < IconTicket width = { 13 } />
381+ < span className = "credit-item-label" >
382+ { isNext ? t ( "codexAuth.creditNext" ) : t ( "codexAuth.creditLabel" , { n : String ( index + 1 ) } ) }
383+ </ span >
384+ { isNext && < span className = "badge badge-amber" style = { { fontSize : 10 , padding : "1px 6px" } } > { t ( "codexAuth.creditNextBadge" ) } </ span > }
385+ </ div >
386+ < div className = "credit-item-dates" >
387+ < span > { t ( "codexAuth.creditGranted" , { date : formatCreditDate ( grantedAt ) } ) } </ span >
388+ < span className = { urgent ? "credit-urgent" : "" } > { t ( "codexAuth.creditExpires" , { date : formatCreditDate ( expiresAt ) , days : String ( days ) } ) } </ span >
389+ </ div >
390+ </ div >
391+ ) ;
392+ }
393+
331394function isWorkspaceAccount ( plan ?: string ) : boolean {
332395 if ( ! plan ) return false ;
333396 return [ "team" , "business" , "enterprise" , "edu" ,
0 commit comments