diff --git a/__test__/features/user/HandleLogin.test.tsx b/__test__/features/user/HandleLogin.test.tsx
index 5e043de..b55e46b 100644
--- a/__test__/features/user/HandleLogin.test.tsx
+++ b/__test__/features/user/HandleLogin.test.tsx
@@ -1,10 +1,12 @@
import * as appHooks from "@/app/hooks";
-import { AppDispatch } from "@/app/store";
+import { AppDispatch, setupStore } from "@/app/store";
import HandleLogin from "@/features/User/HandleLogin";
import * as oidcAuth from "@/features/User/oidcAuth";
import { clientConfig } from "@/features/User/oidcAuth";
+import { useInitializeApp } from "@/features/User/useInitializeApp";
import * as apiUtils from "@/utils/apiUtils";
-import { screen, waitFor } from "@testing-library/react";
+import { renderHook, screen, waitFor } from "@testing-library/react";
+import { Provider } from "react-redux";
import { push } from "redux-first-history";
import { renderWithProviders } from "../../utils/Renderwithproviders";
@@ -17,6 +19,17 @@ describe("HandleLogin", () => {
const dispatch = jest.fn() as AppDispatch;
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
sessionStorage.clear();
+ Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: jest.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ })),
+ });
});
test("redirects and sets sessionStorage when no userData", async () => {
@@ -28,18 +41,23 @@ describe("HandleLogin", () => {
jest.spyOn(oidcAuth, "Auth").mockResolvedValue(loginUrlMock);
- renderWithProviders(, {
- user: {
- userData: null,
- tokens: null,
- loading: false,
- error: null,
- },
- calendars: {
- list: {},
- pending: false,
- error: null,
- },
+ const { result } = renderHook(() => useInitializeApp(), {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
});
await waitFor(
diff --git a/src/App.tsx b/src/App.tsx
index e19b71f..b0ad299 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -13,6 +13,7 @@ import { Loading } from "./components/Loading/Loading";
import { AVAILABLE_LANGUAGES } from "./features/Settings/constants";
import HandleLogin from "./features/User/HandleLogin";
import { CallbackResume } from "./features/User/LoginCallback";
+import { useInitializeApp } from "./features/User/useInitializeApp";
import { ScreenTooSmall } from "./ScreenTooSmall";
import { WebSocketGate } from "./websocket/WebSocketGate";
@@ -68,6 +69,8 @@ function App() {
() => window.matchMedia(SMALL_SCREEN_QUERY).matches
);
+ useInitializeApp();
+
useEffect(() => {
const mediaQuery = window.matchMedia(SMALL_SCREEN_QUERY);
const onChange = (event: MediaQueryListEvent) =>
diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts
index 27cd547..79cdcfe 100644
--- a/src/features/Calendars/CalendarSlice.ts
+++ b/src/features/Calendars/CalendarSlice.ts
@@ -126,10 +126,25 @@ const CalendarSlice = createSlice({
}>
) => {
state.pending = false;
- state.list = action.payload.importedCalendars;
state.error = action.payload.errors.length
? action.payload.errors
: null;
+
+ Object.entries(action.payload.importedCalendars).forEach(
+ ([id, cal]) => {
+ state.list[id] = {
+ ...cal,
+ events: state.list[id]?.events || {},
+ };
+ }
+ );
+
+ // Remove calendars that no longer exist
+ Object.keys(state.list).forEach((id) => {
+ if (!action.payload.importedCalendars[id]) {
+ delete state.list[id];
+ }
+ });
}
)
.addCase(
diff --git a/src/features/User/HandleLogin.tsx b/src/features/User/HandleLogin.tsx
index e24160e..b8675ec 100644
--- a/src/features/User/HandleLogin.tsx
+++ b/src/features/User/HandleLogin.tsx
@@ -1,60 +1,14 @@
import { useAppDispatch, useAppSelector } from "@/app/hooks";
-import { redirectTo } from "@/utils/apiUtils";
import { useEffect, useRef } from "react";
import { push } from "redux-first-history";
-import { getCalendarsListAsync } from "../Calendars/services";
-import { Auth } from "./oidcAuth";
-import { getOpenPaasUserDataAsync, setTokens, setUserData } from "./userSlice";
import { setAppLoading } from "@/app/loadingSlice";
export function HandleLogin() {
const userData = useAppSelector((state) => state.user);
const calendars = useAppSelector((state) => state.calendars);
const dispatch = useAppDispatch();
- const hasInitiatedRef = useRef(false);
const hasNavigatedRef = useRef(false);
- // Initiate login or load saved data
- useEffect(() => {
- if (hasInitiatedRef.current) return;
- if (userData.userData && !calendars.pending) return;
-
- hasInitiatedRef.current = true;
-
- const initiateLogin = async () => {
- const savedToken = sessionStorage.getItem("tokenSet")
- ? JSON.parse(sessionStorage.getItem("tokenSet")!)
- : null;
- const savedUser = sessionStorage.getItem("userData")
- ? JSON.parse(sessionStorage.getItem("userData")!)
- : null;
-
- if (savedToken && savedUser) {
- dispatch(setAppLoading(true));
- dispatch(setTokens(savedToken));
- dispatch(setUserData(savedUser));
- await dispatch(getOpenPaasUserDataAsync());
- await dispatch(getCalendarsListAsync());
- return;
- }
-
- const loginurl = await Auth();
-
- sessionStorage.setItem(
- "redirectState",
- JSON.stringify({
- code_verifier: loginurl.code_verifier,
- state: loginurl.state,
- })
- );
-
- redirectTo(loginurl.redirectTo);
- };
-
- initiateLogin();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [userData.userData, calendars.list, dispatch]);
-
// Navigate to /calendar only when all data is ready
useEffect(() => {
if (hasNavigatedRef.current) return;
diff --git a/src/features/User/useInitializeApp.ts b/src/features/User/useInitializeApp.ts
new file mode 100644
index 0000000..2397624
--- /dev/null
+++ b/src/features/User/useInitializeApp.ts
@@ -0,0 +1,61 @@
+import { useAppDispatch, useAppSelector } from "@/app/hooks";
+import { setAppLoading } from "@/app/loadingSlice";
+import { getCalendarsListAsync } from "@/features/Calendars/services";
+import { Auth } from "@/features/User/oidcAuth";
+import {
+ getOpenPaasUserDataAsync,
+ setTokens,
+ setUserData,
+} from "@/features/User/userSlice";
+import { redirectTo } from "@/utils/apiUtils";
+import { useEffect, useRef } from "react";
+
+export function useInitializeApp() {
+ const userData = useAppSelector((state) => state.user);
+ const calendars = useAppSelector((state) => state.calendars);
+ const dispatch = useAppDispatch();
+ const hasInitiatedRef = useRef(false);
+
+ useEffect(() => {
+ if (hasInitiatedRef.current) return;
+ if (userData.userData && !calendars.pending) return;
+ if (window.location.pathname === "/callback") return;
+ hasInitiatedRef.current = true;
+
+ const initiateLogin = async () => {
+ const savedToken = sessionStorage.getItem("tokenSet")
+ ? JSON.parse(sessionStorage.getItem("tokenSet")!)
+ : null;
+ const savedUser = sessionStorage.getItem("userData")
+ ? JSON.parse(sessionStorage.getItem("userData")!)
+ : null;
+
+ if (savedToken && savedUser) {
+ dispatch(setAppLoading(true));
+ dispatch(setTokens(savedToken));
+ dispatch(setUserData(savedUser));
+ try {
+ await dispatch(getOpenPaasUserDataAsync());
+ await dispatch(getCalendarsListAsync());
+ } finally {
+ dispatch(setAppLoading(false));
+ }
+
+ return;
+ }
+
+ const loginurl = await Auth();
+ sessionStorage.setItem(
+ "redirectState",
+ JSON.stringify({
+ code_verifier: loginurl.code_verifier,
+ state: loginurl.state,
+ })
+ );
+ redirectTo(loginurl.redirectTo);
+ };
+
+ initiateLogin();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [userData.userData]);
+}