Skip to content

Commit 6e5690c

Browse files
committed
fix: ensure kinde logout is triggered and navigation history is counted.
1 parent dde1e9a commit 6e5690c

File tree

1 file changed

+105
-6
lines changed

1 file changed

+105
-6
lines changed

src/state/KindeProvider.tsx

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
setActiveStorage,
3232
isAuthenticated,
3333
updateActivityTimestamp,
34+
getInsecureStorage,
3435
} from "@kinde/js-utils";
3536
import * as storeState from "./store";
3637
import React, {
@@ -48,6 +49,7 @@ import {
4849
LogoutOptions,
4950
PopupOptions,
5051
ActivityTimeoutConfig,
52+
TimeoutActivityType,
5153
} from "./types";
5254
import 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

128130
const 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+
148199
export 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

Comments
 (0)