@chemmangat/msal-next 5.3.2 → 5.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [5.3.4] - 2026-05-06
6
+
7
+ ### 🐛 Bug Fix
8
+
9
+ #### `useTokenRefresh` — `expiresIn` and `isExpiringSoon` are now reactive
10
+
11
+ `expiresIn` and `isExpiringSoon` were previously derived from `useRef` values, which never trigger a re-render. Any component calling `useTokenRefresh()` to display a session-expiry warning would always see the initial `null` / `false` values and never update.
12
+
13
+ Both values are now backed by `useState`, so components re-render correctly when the token expiry changes.
14
+
15
+ ```tsx
16
+ // This now works as expected — the warning will actually appear
17
+ const { isExpiringSoon, expiresIn } = useTokenRefresh({ refreshBeforeExpiry: 300 });
18
+
19
+ if (isExpiringSoon) {
20
+ return <div>⚠️ Your session expires in {Math.round(expiresIn!)} seconds</div>;
21
+ }
22
+ ```
23
+
24
+ `lastRefresh` is also now a state value, so it updates in the UI after each refresh.
25
+
26
+ ---
27
+
5
28
  ## [5.3.0] - 2026-04-07
6
29
 
7
30
  ### 🐛 Bug Fixes
package/dist/index.js CHANGED
@@ -857,6 +857,8 @@ function useTokenRefresh(options = {}) {
857
857
  const intervalRef = (0, import_react2.useRef)(null);
858
858
  const lastRefreshRef = (0, import_react2.useRef)(null);
859
859
  const expiresInRef = (0, import_react2.useRef)(null);
860
+ const [expiresIn, setExpiresIn] = (0, import_react2.useState)(null);
861
+ const [lastRefresh, setLastRefresh] = (0, import_react2.useState)(null);
860
862
  const refresh = (0, import_react2.useCallback)(async () => {
861
863
  if (!isAuthenticated || !account) {
862
864
  return;
@@ -868,9 +870,11 @@ function useTokenRefresh(options = {}) {
868
870
  forceRefresh: false
869
871
  });
870
872
  lastRefreshRef.current = /* @__PURE__ */ new Date();
871
- const expiresIn = response.expiresOn ? Math.max(0, response.expiresOn.getTime() / 1e3 - Date.now() / 1e3) : 3600;
872
- expiresInRef.current = expiresIn;
873
- onRefresh?.(expiresIn);
873
+ const newExpiresIn = response.expiresOn ? Math.max(0, response.expiresOn.getTime() / 1e3 - Date.now() / 1e3) : 3600;
874
+ expiresInRef.current = newExpiresIn;
875
+ setExpiresIn(newExpiresIn);
876
+ setLastRefresh(lastRefreshRef.current);
877
+ onRefresh?.(newExpiresIn);
874
878
  } catch (error) {
875
879
  console.error("[TokenRefresh] Failed to refresh token:", error);
876
880
  onError?.(error);
@@ -888,6 +892,7 @@ function useTokenRefresh(options = {}) {
888
892
  const timeSinceRefresh = lastRefreshRef.current ? (Date.now() - lastRefreshRef.current.getTime()) / 1e3 : 0;
889
893
  const remainingTime = expiresInRef.current - timeSinceRefresh;
890
894
  expiresInRef.current = Math.max(0, remainingTime);
895
+ setExpiresIn(expiresInRef.current);
891
896
  if (remainingTime <= refreshBeforeExpiry && remainingTime > 0) {
892
897
  refresh();
893
898
  }
@@ -898,12 +903,12 @@ function useTokenRefresh(options = {}) {
898
903
  }
899
904
  };
900
905
  }, [enabled, isAuthenticated, refreshBeforeExpiry, refresh]);
901
- const isExpiringSoon = expiresInRef.current !== null && expiresInRef.current <= refreshBeforeExpiry;
906
+ const isExpiringSoon = expiresIn !== null && expiresIn <= refreshBeforeExpiry;
902
907
  return {
903
- expiresIn: expiresInRef.current,
908
+ expiresIn,
904
909
  isExpiringSoon,
905
910
  refresh,
906
- lastRefresh: lastRefreshRef.current
911
+ lastRefresh
907
912
  };
908
913
  }
909
914
 
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
9
9
  // src/components/MsalAuthProvider.tsx
10
10
  import { MsalProvider } from "@azure/msal-react";
11
11
  import { PublicClientApplication, EventType } from "@azure/msal-browser";
12
- import { useEffect as useEffect2, useState, useRef as useRef2 } from "react";
12
+ import { useEffect as useEffect2, useState as useState2, useRef as useRef2 } from "react";
13
13
 
14
14
  // src/utils/createMsalConfig.ts
15
15
  import { LogLevel } from "@azure/msal-browser";
@@ -613,7 +613,7 @@ Note: Environment variables starting with NEXT_PUBLIC_ are exposed to the browse
613
613
  }
614
614
 
615
615
  // src/hooks/useTokenRefresh.ts
616
- import { useEffect, useRef, useCallback as useCallback2 } from "react";
616
+ import { useEffect, useRef, useCallback as useCallback2, useState } from "react";
617
617
  import { useMsal as useMsal2 } from "@azure/msal-react";
618
618
 
619
619
  // src/hooks/useMsalAuth.ts
@@ -798,6 +798,8 @@ function useTokenRefresh(options = {}) {
798
798
  const intervalRef = useRef(null);
799
799
  const lastRefreshRef = useRef(null);
800
800
  const expiresInRef = useRef(null);
801
+ const [expiresIn, setExpiresIn] = useState(null);
802
+ const [lastRefresh, setLastRefresh] = useState(null);
801
803
  const refresh = useCallback2(async () => {
802
804
  if (!isAuthenticated || !account) {
803
805
  return;
@@ -809,9 +811,11 @@ function useTokenRefresh(options = {}) {
809
811
  forceRefresh: false
810
812
  });
811
813
  lastRefreshRef.current = /* @__PURE__ */ new Date();
812
- const expiresIn = response.expiresOn ? Math.max(0, response.expiresOn.getTime() / 1e3 - Date.now() / 1e3) : 3600;
813
- expiresInRef.current = expiresIn;
814
- onRefresh?.(expiresIn);
814
+ const newExpiresIn = response.expiresOn ? Math.max(0, response.expiresOn.getTime() / 1e3 - Date.now() / 1e3) : 3600;
815
+ expiresInRef.current = newExpiresIn;
816
+ setExpiresIn(newExpiresIn);
817
+ setLastRefresh(lastRefreshRef.current);
818
+ onRefresh?.(newExpiresIn);
815
819
  } catch (error) {
816
820
  console.error("[TokenRefresh] Failed to refresh token:", error);
817
821
  onError?.(error);
@@ -829,6 +833,7 @@ function useTokenRefresh(options = {}) {
829
833
  const timeSinceRefresh = lastRefreshRef.current ? (Date.now() - lastRefreshRef.current.getTime()) / 1e3 : 0;
830
834
  const remainingTime = expiresInRef.current - timeSinceRefresh;
831
835
  expiresInRef.current = Math.max(0, remainingTime);
836
+ setExpiresIn(expiresInRef.current);
832
837
  if (remainingTime <= refreshBeforeExpiry && remainingTime > 0) {
833
838
  refresh();
834
839
  }
@@ -839,12 +844,12 @@ function useTokenRefresh(options = {}) {
839
844
  }
840
845
  };
841
846
  }, [enabled, isAuthenticated, refreshBeforeExpiry, refresh]);
842
- const isExpiringSoon = expiresInRef.current !== null && expiresInRef.current <= refreshBeforeExpiry;
847
+ const isExpiringSoon = expiresIn !== null && expiresIn <= refreshBeforeExpiry;
843
848
  return {
844
- expiresIn: expiresInRef.current,
849
+ expiresIn,
845
850
  isExpiringSoon,
846
851
  refresh,
847
- lastRefresh: lastRefreshRef.current
852
+ lastRefresh
848
853
  };
849
854
  }
850
855
 
@@ -985,7 +990,7 @@ function MsalAuthProvider({
985
990
  onTenantDenied,
986
991
  ...config
987
992
  }) {
988
- const [msalInstance, setMsalInstance] = useState(null);
993
+ const [msalInstance, setMsalInstance] = useState2(null);
989
994
  const instanceRef = useRef2(null);
990
995
  const { scopes = ["User.Read"], enableLogging = false } = config;
991
996
  useEffect2(() => {
@@ -1155,7 +1160,7 @@ function MSALProvider({ children, protection, onTenantDenied, ...props }) {
1155
1160
  }
1156
1161
 
1157
1162
  // src/components/MicrosoftSignInButton.tsx
1158
- import { useState as useState2, useEffect as useEffect3, useRef as useRef3 } from "react";
1163
+ import { useState as useState3, useEffect as useEffect3, useRef as useRef3 } from "react";
1159
1164
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1160
1165
  function MicrosoftSignInButton({
1161
1166
  text = "Sign in with Microsoft",
@@ -1168,7 +1173,7 @@ function MicrosoftSignInButton({
1168
1173
  onError
1169
1174
  }) {
1170
1175
  const { loginRedirect, inProgress, isAuthenticated } = useMsalAuth();
1171
- const [isLoading, setIsLoading] = useState2(false);
1176
+ const [isLoading, setIsLoading] = useState3(false);
1172
1177
  const timeoutRef = useRef3(null);
1173
1178
  useEffect3(() => {
1174
1179
  return () => {
@@ -1356,10 +1361,10 @@ function MicrosoftLogo2() {
1356
1361
  }
1357
1362
 
1358
1363
  // src/components/UserAvatar.tsx
1359
- import { useEffect as useEffect5, useState as useState4 } from "react";
1364
+ import { useEffect as useEffect5, useState as useState5 } from "react";
1360
1365
 
1361
1366
  // src/hooks/useUserProfile.ts
1362
- import { useState as useState3, useEffect as useEffect4, useCallback as useCallback4 } from "react";
1367
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4 } from "react";
1363
1368
 
1364
1369
  // src/hooks/useGraphApi.ts
1365
1370
  import { useCallback as useCallback3 } from "react";
@@ -1490,9 +1495,9 @@ function enforceCacheLimit() {
1490
1495
  function useUserProfile() {
1491
1496
  const { isAuthenticated, account } = useMsalAuth();
1492
1497
  const graph = useGraphApi();
1493
- const [profile, setProfile] = useState3(null);
1494
- const [loading, setLoading] = useState3(false);
1495
- const [error, setError] = useState3(null);
1498
+ const [profile, setProfile] = useState4(null);
1499
+ const [loading, setLoading] = useState4(false);
1500
+ const [error, setError] = useState4(null);
1496
1501
  const fetchProfile = useCallback4(async () => {
1497
1502
  if (!isAuthenticated || !account) {
1498
1503
  setProfile(null);
@@ -1626,8 +1631,8 @@ function UserAvatar({
1626
1631
  fallbackImage
1627
1632
  }) {
1628
1633
  const { profile, loading } = useUserProfile();
1629
- const [photoUrl, setPhotoUrl] = useState4(null);
1630
- const [photoError, setPhotoError] = useState4(false);
1634
+ const [photoUrl, setPhotoUrl] = useState5(null);
1635
+ const [photoError, setPhotoError] = useState5(false);
1631
1636
  useEffect5(() => {
1632
1637
  if (profile?.photo) {
1633
1638
  setPhotoUrl(profile.photo);
@@ -1907,10 +1912,10 @@ var ErrorBoundary = class extends Component {
1907
1912
  // src/hooks/useMultiAccount.ts
1908
1913
  import { useMsal as useMsal3 } from "@azure/msal-react";
1909
1914
  import { InteractionStatus as InteractionStatus2 } from "@azure/msal-browser";
1910
- import { useCallback as useCallback5, useMemo as useMemo2, useState as useState5, useEffect as useEffect7 } from "react";
1915
+ import { useCallback as useCallback5, useMemo as useMemo2, useState as useState6, useEffect as useEffect7 } from "react";
1911
1916
  function useMultiAccount(defaultScopes = ["User.Read"]) {
1912
1917
  const { instance, accounts, inProgress } = useMsal3();
1913
- const [activeAccount, setActiveAccount] = useState5(
1918
+ const [activeAccount, setActiveAccount] = useState6(
1914
1919
  instance.getActiveAccount()
1915
1920
  );
1916
1921
  useEffect7(() => {
@@ -2060,7 +2065,7 @@ function useMultiAccount(defaultScopes = ["User.Read"]) {
2060
2065
  }
2061
2066
 
2062
2067
  // src/components/AccountSwitcher.tsx
2063
- import { useState as useState6 } from "react";
2068
+ import { useState as useState7 } from "react";
2064
2069
  import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
2065
2070
  function AccountSwitcher({
2066
2071
  showAvatars = true,
@@ -2084,8 +2089,8 @@ function AccountSwitcher({
2084
2089
  isActiveAccount,
2085
2090
  accountCount
2086
2091
  } = useMultiAccount();
2087
- const [isOpen, setIsOpen] = useState6(false);
2088
- const [removingAccount, setRemovingAccount] = useState6(null);
2092
+ const [isOpen, setIsOpen] = useState7(false);
2093
+ const [removingAccount, setRemovingAccount] = useState7(null);
2089
2094
  const handleSwitch = (account) => {
2090
2095
  switchAccount(account);
2091
2096
  setIsOpen(false);
@@ -2614,7 +2619,7 @@ function useTenant() {
2614
2619
  }
2615
2620
 
2616
2621
  // src/hooks/useRoles.ts
2617
- import { useState as useState7, useEffect as useEffect8, useCallback as useCallback6 } from "react";
2622
+ import { useState as useState8, useEffect as useEffect8, useCallback as useCallback6 } from "react";
2618
2623
  var rolesCache = /* @__PURE__ */ new Map();
2619
2624
  var CACHE_DURATION2 = 5 * 60 * 1e3;
2620
2625
  var MAX_CACHE_SIZE2 = 100;
@@ -2636,10 +2641,10 @@ function enforceCacheLimit2() {
2636
2641
  function useRoles() {
2637
2642
  const { isAuthenticated, account } = useMsalAuth();
2638
2643
  const graph = useGraphApi();
2639
- const [roles, setRoles] = useState7([]);
2640
- const [groups, setGroups] = useState7([]);
2641
- const [loading, setLoading] = useState7(false);
2642
- const [error, setError] = useState7(null);
2644
+ const [roles, setRoles] = useState8([]);
2645
+ const [groups, setGroups] = useState8([]);
2646
+ const [loading, setLoading] = useState8(false);
2647
+ const [error, setError] = useState8(null);
2643
2648
  const fetchRolesAndGroups = useCallback6(async () => {
2644
2649
  if (!isAuthenticated || !account) {
2645
2650
  setRoles([]);
@@ -3005,7 +3010,7 @@ function createScopedLogger(scope, config) {
3005
3010
  }
3006
3011
 
3007
3012
  // src/protection/ProtectedPage.tsx
3008
- import { useEffect as useEffect9, useState as useState8 } from "react";
3013
+ import { useEffect as useEffect9, useState as useState9 } from "react";
3009
3014
  import { useRouter } from "next/navigation";
3010
3015
  import { Fragment as Fragment6, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
3011
3016
  function ProtectedPage({
@@ -3019,8 +3024,8 @@ function ProtectedPage({
3019
3024
  const router = useRouter();
3020
3025
  const { isAuthenticated, account, inProgress } = useMsalAuth();
3021
3026
  const tenantInfo = useTenant();
3022
- const [isValidating, setIsValidating] = useState8(true);
3023
- const [isAuthorized, setIsAuthorized] = useState8(false);
3027
+ const [isValidating, setIsValidating] = useState9(true);
3028
+ const [isAuthorized, setIsAuthorized] = useState9(false);
3024
3029
  useEffect9(() => {
3025
3030
  async function checkAuth() {
3026
3031
  if (debug) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chemmangat/msal-next",
3
- "version": "5.3.2",
3
+ "version": "5.3.4",
4
4
  "description": "Production-ready Microsoft/Azure AD authentication for Next.js App Router. Zero-config setup, TypeScript-first, multi-account support, auto token refresh. The easiest way to add Microsoft login to your Next.js app.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",