@@ -31,6 +31,7 @@ import {
3131 setActiveStorage ,
3232 isAuthenticated ,
3333 updateActivityTimestamp ,
34+ getInsecureStorage ,
3435} from "@kinde/js-utils" ;
3536import * as storeState from "./store" ;
3637import React , {
@@ -48,6 +49,7 @@ import {
4849 LogoutOptions ,
4950 PopupOptions ,
5051 ActivityTimeoutConfig ,
52+ TimeoutActivityType ,
5153} from "./types" ;
5254import type {
5355 RefreshTokenResult ,
@@ -122,7 +124,7 @@ type KindeProviderProps = {
122124 * ⚠️ Must be memoized or defined outside component to prevent effect re-runs.
123125 */
124126 activityTimeout ?: ActivityTimeoutConfig ;
125- refreshOnFocus ?: boolean
127+ refreshOnFocus ?: boolean ;
126128} ;
127129
128130const defaultCallbacks : KindeCallbacks = {
@@ -145,6 +147,55 @@ type ProviderState = {
145147 isLoading : boolean ;
146148} ;
147149
150+ type Options = { skipInitial ?: boolean } ;
151+
152+ const useOnLocationChange = (
153+ run : ( loc : Location ) => void ,
154+ { skipInitial = false } : Options = { } ,
155+ ) => {
156+ const initial = useRef ( true ) ;
157+
158+ useEffect ( ( ) => {
159+ const notify = ( ) => {
160+ if ( skipInitial && initial . current ) {
161+ initial . current = false ;
162+ return ;
163+ }
164+ run ( window . location ) ;
165+ } ;
166+
167+ // back/forward
168+ const onPop = ( ) => notify ( ) ;
169+ window . addEventListener ( "popstate" , onPop ) ;
170+ window . addEventListener ( "hashchange" , onPop ) ;
171+
172+ // pushState/replaceState don't emit events: patch them
173+ const origPush = history . pushState ;
174+ const origReplace = history . replaceState ;
175+
176+ history . pushState = function ( ...args ) {
177+ origPush . apply ( this , args ) ;
178+ notify ( ) ;
179+ } ;
180+ history . replaceState = function ( ...args ) {
181+ origReplace . apply ( this , args ) ;
182+ notify ( ) ;
183+ } ;
184+
185+ // Optional: Navigation API (Chromium, evolving support)
186+ // const nav: any = (window as any).navigation;
187+ // if (nav?.addEventListener) nav.addEventListener('navigate', notify);
188+
189+ return ( ) => {
190+ window . removeEventListener ( "popstate" , onPop ) ;
191+ window . removeEventListener ( "hashchange" , onPop ) ;
192+ history . pushState = origPush ;
193+ history . replaceState = origReplace ;
194+ // if (nav?.removeEventListener) nav.removeEventListener('navigate', notify);
195+ } ;
196+ } , [ run , skipInitial ] ) ;
197+ } ;
198+
148199export const KindeProvider = ( {
149200 audience,
150201 scope,
@@ -163,6 +214,10 @@ export const KindeProvider = ({
163214} : KindeProviderProps ) => {
164215 const mergedCallbacks = { ...defaultCallbacks , ...callbacks } ;
165216
217+ useOnLocationChange ( ( ) => {
218+ updateActivityTimestamp ( ) ;
219+ } ) ;
220+
166221 useEffect ( ( ) => {
167222 setActiveStorage ( store ) ;
168223
@@ -174,8 +229,49 @@ export const KindeProvider = ({
174229 storageSettings . activityTimeoutMinutes = activityTimeout . timeoutMinutes ;
175230 storageSettings . activityTimeoutPreWarningMinutes =
176231 activityTimeout . preWarningMinutes ;
177- storageSettings . onActivityTimeout = activityTimeout . onTimeout ;
178- setActiveStorage ( store ) ;
232+ storageSettings . onActivityTimeout = async ( type : TimeoutActivityType ) => {
233+ try {
234+ if ( type === TimeoutActivityType . timeout ) {
235+ const insecureStorage = getInsecureStorage ( ) ;
236+ const accessToken = await store . getSessionItem (
237+ StorageKeys . accessToken ,
238+ ) ;
239+ const refreshToken = await insecureStorage ?. getSessionItem (
240+ StorageKeys . refreshToken ,
241+ ) ;
242+ await Promise . all ( [
243+ await fetch ( `${ domain } /logout` ) ,
244+ refreshToken &&
245+ ( await fetch ( `${ domain } /oauth2/revoke` , {
246+ method : "POST" ,
247+ body : JSON . stringify ( {
248+ token : await store . getSessionItem ( StorageKeys . accessToken ) ,
249+ } ) ,
250+ headers : {
251+ "Content-Type" : "application/json" ,
252+ Authorization : `Bearer ${ await store . getSessionItem ( StorageKeys . accessToken ) } ` ,
253+ } ,
254+ } ) ) ,
255+ ] ) ;
256+ if ( accessToken ) {
257+ await fetch ( `${ domain } /oauth2/revoke` , {
258+ method : "POST" ,
259+ body : JSON . stringify ( {
260+ token : await store . getSessionItem ( StorageKeys . accessToken ) ,
261+ } ) ,
262+ headers : {
263+ "Content-Type" : "application/json" ,
264+ Authorization : `Bearer ${ await store . getSessionItem ( StorageKeys . accessToken ) } ` ,
265+ } ,
266+ } ) ;
267+ }
268+ }
269+ } catch ( error ) {
270+ console . error ( "Failed to logout:" , error ) ;
271+ } finally {
272+ activityTimeout . onTimeout ?.( type ) ;
273+ }
274+ } ;
179275 try {
180276 updateActivityTimestamp ( ) ;
181277 } catch ( error ) {
@@ -191,7 +287,6 @@ export const KindeProvider = ({
191287 storageSettings . activityTimeoutMinutes = undefined ;
192288 storageSettings . activityTimeoutPreWarningMinutes = undefined ;
193289 storageSettings . onActivityTimeout = undefined ;
194- setActiveStorage ( store ) ;
195290 isTrackingEnabled = false ;
196291 } ;
197292
@@ -607,7 +702,11 @@ export const KindeProvider = ({
607702 ) ;
608703
609704 const handleFocus = useCallback ( ( ) => {
610- if ( document . visibilityState === "visible" && state . isAuthenticated && refreshOnFocus ) {
705+ if (
706+ document . visibilityState === "visible" &&
707+ state . isAuthenticated &&
708+ refreshOnFocus
709+ ) {
611710 refreshToken ( { domain, clientId, onRefresh } ) . catch ( ( error ) => {
612711 console . error ( "Error refreshing token:" , error ) ;
613712 } ) ;
@@ -619,7 +718,7 @@ export const KindeProvider = ({
619718
620719 document . removeEventListener ( "visibilitychange" , handleFocus ) ;
621720 if ( refreshOnFocus ) {
622- document . addEventListener ( "visibilitychange" , handleFocus ) ;
721+ document . addEventListener ( "visibilitychange" , handleFocus ) ;
623722 return ( ) => {
624723 document . removeEventListener ( "visibilitychange" , handleFocus ) ;
625724 } ;
0 commit comments