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]); +}