@@ -8,7 +8,13 @@ import { ExclamationCircleOutlined } from "@ant-design/icons";
88
99import { useAuth } from "@/hooks/useAuth" ;
1010import { authService } from "@/services/authService" ;
11+ import { sessionService } from "@/services/sessionService" ;
12+ import { getSessionFromStorage } from "@/lib/auth" ;
1113import { EVENTS } from "@/const/auth" ;
14+ import {
15+ TOKEN_REFRESH_BEFORE_EXPIRY_MS ,
16+ MIN_ACTIVITY_CHECK_INTERVAL_MS ,
17+ } from "@/const/constants" ;
1218import log from "@/lib/logger" ;
1319
1420/**
@@ -129,14 +135,89 @@ export function SessionListeners() {
129135 detail : { message : "Session expired, please sign in again" } ,
130136 } )
131137 ) ;
138+ } else if ( ! session && ! hadLocalSession ) {
139+ // Full mode with no prior session: proactively prompt login
140+ openLoginModal ( ) ;
132141 }
133142 } catch ( error ) {
134143 log . error ( "Error checking session status:" , error ) ;
135144 }
136145 } ;
137146
138147 checkSession ( ) ;
139- } , [ pathname ] ) ;
148+ } , [ pathname , isSpeedMode , openLoginModal ] ) ;
149+
150+ // Sliding expiration: refresh token shortly before expiry on user activity (skip in speed mode)
151+ useEffect ( ( ) => {
152+ if ( isSpeedMode ) return ;
153+
154+ let lastActivityCheckAt = 0 ;
155+
156+ const maybeRefreshOnActivity = async ( ) => {
157+ try {
158+ // Throttle activity-driven checks
159+ const now = Date . now ( ) ;
160+ if ( now - lastActivityCheckAt < MIN_ACTIVITY_CHECK_INTERVAL_MS ) return ;
161+ lastActivityCheckAt = now ;
162+
163+ // Do not run when page is hidden
164+ if ( typeof document !== "undefined" && document . hidden ) return ;
165+
166+ const sessionObj = getSessionFromStorage ( ) ;
167+ if ( ! sessionObj ?. expires_at ) return ;
168+
169+ const msUntilExpiry = sessionObj . expires_at * 1000 - now ;
170+ if ( msUntilExpiry <= TOKEN_REFRESH_BEFORE_EXPIRY_MS ) {
171+ const ok = await sessionService . checkAndRefreshToken ( ) ;
172+ if ( ! ok ) {
173+ // If refresh failed and token is already expired, raise expired flow
174+ if ( msUntilExpiry <= 0 ) {
175+ window . dispatchEvent (
176+ new CustomEvent ( EVENTS . SESSION_EXPIRED , {
177+ detail : { message : "Session expired, please sign in again" } ,
178+ } )
179+ ) ;
180+ }
181+ }
182+ }
183+ } catch ( error ) {
184+ log . error ( "Activity-based refresh check failed:" , error ) ;
185+ }
186+ } ;
187+
188+ const events : ( keyof DocumentEventMap | keyof WindowEventMap ) [ ] = [
189+ "click" ,
190+ "keydown" ,
191+ "mousemove" ,
192+ "touchstart" ,
193+ "focus" ,
194+ "visibilitychange" ,
195+ ] ;
196+
197+ const handler = ( ) => {
198+ // Wrap to avoid passing the event into async function
199+ void maybeRefreshOnActivity ( ) ;
200+ } ;
201+
202+ events . forEach ( ( evt ) => {
203+ // Use window for focus/visibility, document for input/mouse
204+ if ( evt === "focus" || evt === "visibilitychange" ) {
205+ window . addEventListener ( evt as any , handler , { passive : true } ) ;
206+ } else {
207+ document . addEventListener ( evt as any , handler , { passive : true } ) ;
208+ }
209+ } ) ;
210+
211+ return ( ) => {
212+ events . forEach ( ( evt ) => {
213+ if ( evt === "focus" || evt === "visibilitychange" ) {
214+ window . removeEventListener ( evt as any , handler as any ) ;
215+ } else {
216+ document . removeEventListener ( evt as any , handler as any ) ;
217+ }
218+ } ) ;
219+ } ;
220+ } , [ isSpeedMode ] ) ;
140221
141222 // This component doesn't render UI elements
142223 return null ;
0 commit comments