diff --git a/CHANGELOG.md b/CHANGELOG.md index dc12d17f7..bd74b087e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed +- Auth web component from user in local storage (#852) - Save and download panel copy (#784) - Application of styles in the web component to remove `sass-to-string` (#788) - Info panel links open in a new tab (#803) diff --git a/src/containers/WebComponentLoader.jsx b/src/containers/WebComponentLoader.jsx index 846674811..273c13dda 100644 --- a/src/containers/WebComponentLoader.jsx +++ b/src/containers/WebComponentLoader.jsx @@ -7,7 +7,6 @@ import { setInstructions } from "../redux/InstructionsSlice"; import { useProject } from "../hooks/useProject"; import { useEmbeddedMode } from "../hooks/useEmbeddedMode"; import { useProjectPersistence } from "../hooks/useProjectPersistence"; -import { removeUser, setUser } from "../redux/WebComponentAuthSlice"; import { SettingsContext } from "../utils/settings"; import { useCookies } from "react-cookie"; import NewFileModal from "../components/Modals/NewFileModal"; @@ -39,7 +38,7 @@ const WebComponentLoader = (props) => { const dispatch = useDispatch(); const { t } = useTranslation(); const [projectIdentifier, setProjectIdentifier] = useState(identifier); - const localStorageUser = JSON.parse(localStorage.getItem(authKey)); + localStorage.setItem("authKey", authKey); const user = useSelector((state) => state.auth.user); const [loadCache, setLoadCache] = useState(!!!user); const [loadRemix, setLoadRemix] = useState(!!user); @@ -77,16 +76,6 @@ const WebComponentLoader = (props) => { } }, [theme, setCookie, dispatch]); - useEffect(() => { - if (JSON.stringify(user) !== JSON.stringify(localStorageUser)) { - if (localStorageUser) { - dispatch(setUser(localStorageUser)); - } else { - dispatch(removeUser()); - } - } - }, [user, localStorageUser, dispatch]); - useEffect(() => { if (remixLoadFailed) { setLoadCache(true); @@ -106,14 +95,14 @@ const WebComponentLoader = (props) => { useProject({ projectIdentifier: projectIdentifier, code, - accessToken: user?.access_token || localStorageUser?.access_token, + accessToken: user?.access_token, loadRemix, loadCache, remixLoadFailed, }); useProjectPersistence({ - user: user?.accessToken ? user : localStorageUser, + user, project, justLoaded, hasShownSavePrompt, diff --git a/src/containers/WebComponentLoader.test.js b/src/containers/WebComponentLoader.test.js index 47dd3d61d..8d4ab5e40 100644 --- a/src/containers/WebComponentLoader.test.js +++ b/src/containers/WebComponentLoader.test.js @@ -4,10 +4,11 @@ import { Provider } from "react-redux"; import configureStore from "redux-mock-store"; import WebComponentLoader from "./WebComponentLoader"; import { disableTheming, setSenseHatAlwaysEnabled } from "../redux/EditorSlice"; -import { removeUser, setUser } from "../redux/WebComponentAuthSlice"; import { setInstructions } from "../redux/InstructionsSlice"; +import { setUser } from "../redux/WebComponentAuthSlice"; import { useProject } from "../hooks/useProject"; import { useProjectPersistence } from "../hooks/useProjectPersistence"; +import localStorageUserMiddleware from "../redux/middlewares/localStorageUserMiddleware"; import { Cookies, CookiesProvider } from "react-cookie"; jest.mock("../hooks/useProject", () => ({ @@ -27,9 +28,9 @@ const instructions = { currentStepPosition: 3, project: { steps: steps } }; const authKey = "my_key"; const user = { access_token: "my_token" }; -describe("When loaded as a cold user", () => { +describe("When no user is in state", () => { beforeEach(() => { - const middlewares = []; + const middlewares = [localStorageUserMiddleware(setUser)]; const mockStore = configureStore(middlewares); const initialState = { editor: { @@ -51,7 +52,7 @@ describe("When loaded as a cold user", () => { cookies = new Cookies(); }); - describe("When props are set - logged out", () => { + describe("with no user in local storage", () => { beforeEach(() => { render( @@ -87,7 +88,7 @@ describe("When loaded as a cold user", () => { }, hasShownSavePrompt: false, justLoaded: false, - user: null, + user: undefined, saveTriggered: false, }); }); @@ -113,11 +114,18 @@ describe("When loaded as a cold user", () => { test("Sets theme correctly", () => { expect(cookies.cookies.theme).toEqual("light"); }); + + test("Sets the user in state", () => { + expect(store.getActions()).toEqual( + expect.arrayContaining([setUser(null)]), + ); + }); }); - describe("When props are set - logged in", () => { + describe("with user set in local storage", () => { beforeEach(() => { localStorage.setItem(authKey, JSON.stringify(user)); + localStorage.setItem("authKey", authKey); render( @@ -138,7 +146,7 @@ describe("When loaded as a cold user", () => { expect(useProject).toHaveBeenCalledWith({ projectIdentifier: identifier, code, - accessToken: "my_token", + accessToken: undefined, loadRemix: false, loadCache: true, remixLoadFailed: false, @@ -147,7 +155,7 @@ describe("When loaded as a cold user", () => { test("Calls useProjectPersistence hook with correct attributes", () => { expect(useProjectPersistence).toHaveBeenCalledWith({ - user, + user: undefined, project: { components: [] }, hasShownSavePrompt: false, justLoaded: false, @@ -196,11 +204,11 @@ describe("When loaded as a cold user", () => { }); }); -describe("When the user object is set", () => { +describe("When user is in state", () => { describe("before a remix load attempt", () => { beforeEach(() => { localStorage.setItem(authKey, JSON.stringify(user)); - const middlewares = []; + const middlewares = [localStorageUserMiddleware(setUser)]; const mockStore = configureStore(middlewares); const initialState = { editor: { @@ -222,7 +230,7 @@ describe("When the user object is set", () => { cookies = new Cookies(); }); - describe("When the user object is deleted", () => { + describe("when user in local storage is removed", () => { beforeEach(() => { localStorage.removeItem(authKey); render( @@ -236,12 +244,12 @@ describe("When the user object is set", () => { test("Removes the user from state", () => { expect(store.getActions()).toEqual( - expect.arrayContaining([removeUser()]), + expect.arrayContaining([setUser(null)]), ); }); }); - describe("When props are set - logged in", () => { + describe("when user is set in local storage", () => { beforeEach(() => { render( @@ -316,7 +324,7 @@ describe("When the user object is set", () => { cookies = new Cookies(); }); - describe("When props are set - logged in", () => { + describe("when user in state and local storage", () => { beforeEach(() => { render( diff --git a/src/redux/middlewares/localStorageUserMiddleware.js b/src/redux/middlewares/localStorageUserMiddleware.js new file mode 100644 index 000000000..86799bb3f --- /dev/null +++ b/src/redux/middlewares/localStorageUserMiddleware.js @@ -0,0 +1,18 @@ +const localStorageUserMiddleware = + (setUser) => (store) => (next) => (action) => { + if (action.type.startsWith("editor")) { + const authKey = localStorage.getItem("authKey"); + if (authKey) { + const localStorageUser = JSON.parse(localStorage.getItem(authKey)); + if ( + JSON.stringify(store.getState().auth.user) !== + JSON.stringify(localStorageUser) + ) { + store.dispatch(setUser(localStorageUser)); + } + } + } + next(action); + }; + +export default localStorageUserMiddleware; diff --git a/src/redux/middlewares/localStorageUserMiddleware.test.js b/src/redux/middlewares/localStorageUserMiddleware.test.js new file mode 100644 index 000000000..a47e43b2c --- /dev/null +++ b/src/redux/middlewares/localStorageUserMiddleware.test.js @@ -0,0 +1,94 @@ +import configureStore from "redux-mock-store"; + +import localStorageUserMiddleware from "./localStorageUserMiddleware"; + +describe(`localStorageUserMiddleware`, () => { + let store; + let next = jest.fn(); + let setUser = jest.fn(); + let user = { user: "some-user" }; + const middleware = [localStorageUserMiddleware(setUser)]; + const mockStore = configureStore({ + reducer: setUser, + middleware, + }); + + describe("as a cold user", () => { + beforeEach(() => { + const initialState = { + editor: {}, + auth: {}, + }; + store = mockStore(initialState); + store.dispatch = jest.fn(); + }); + + describe(`when user is set in local storage`, () => { + beforeEach(() => { + localStorage.setItem("authKey", "some-key"); + localStorage.setItem("some-key", JSON.stringify(user)); + }); + + it(`expects setUser to be called`, () => { + localStorageUserMiddleware(setUser)(store)(next)({ + type: "editor/someAction", + }); + expect(setUser).toBeCalledWith(user); + }); + }); + + describe(`when authKey is not set in local storage`, () => { + beforeEach(() => { + localStorage.removeItem("authKey"); + }); + + it(`expects setUser to not be called`, () => { + localStorageUserMiddleware(setUser)(store)(next)({ + type: "editor/someAction", + }); + expect(setUser).not.toBeCalled(); + }); + }); + }); + + describe("as a logged in user", () => { + const updatedUser = { user: "updated-user" }; + + beforeEach(() => { + const initialState = { + editor: {}, + auth: { user }, + }; + store = mockStore(initialState); + store.dispatch = jest.fn(); + }); + + describe(`when user in local storage has been updated`, () => { + beforeEach(() => { + localStorage.setItem("authKey", "some-key"); + localStorage.setItem("some-key", JSON.stringify(updatedUser)); + }); + + it(`expects setUser to be called`, () => { + localStorageUserMiddleware(setUser)(store)(next)({ + type: "editor/someAction", + }); + expect(setUser).toBeCalledWith(updatedUser); + }); + }); + + describe(`when authKey is not set in local storage`, () => { + beforeEach(() => { + localStorage.removeItem("authKey"); + localStorage.setItem("some-key", JSON.stringify(updatedUser)); + }); + + it(`expects setUser to not be called`, () => { + localStorageUserMiddleware(setUser)(store)(next)({ + type: "editor/someAction", + }); + expect(setUser).not.toBeCalled(); + }); + }); + }); +}); diff --git a/src/app/WebComponentStore.js b/src/redux/stores/WebComponentStore.js similarity index 62% rename from src/app/WebComponentStore.js rename to src/redux/stores/WebComponentStore.js index bf2bde912..4de6eb83f 100644 --- a/src/app/WebComponentStore.js +++ b/src/redux/stores/WebComponentStore.js @@ -1,7 +1,8 @@ import { configureStore } from "@reduxjs/toolkit"; -import EditorReducer from "../redux/EditorSlice"; -import InstructionsReducer from "../redux/InstructionsSlice"; -import WebComponentAuthReducer from "../redux/WebComponentAuthSlice"; +import EditorReducer from "../EditorSlice"; +import InstructionsReducer from "../InstructionsSlice"; +import WebComponentAuthReducer, { setUser } from "../WebComponentAuthSlice"; +import userMiddleWare from "../middlewares/localStorageUserMiddleware"; const store = configureStore({ reducer: { @@ -18,7 +19,7 @@ const store = configureStore({ ], ignoredPaths: ["auth.user"], }, - }), + }).concat(userMiddleWare(setUser)), }); export default store; diff --git a/src/web-component.js b/src/web-component.js index d7cad7449..ad2777bbe 100644 --- a/src/web-component.js +++ b/src/web-component.js @@ -4,7 +4,7 @@ import * as ReactDOMClient from "react-dom/client"; import * as Sentry from "@sentry/react"; import { BrowserTracing } from "@sentry/tracing"; import WebComponentLoader from "./containers/WebComponentLoader"; -import store from "./app/WebComponentStore"; +import store from "./redux/stores/WebComponentStore"; import { Provider } from "react-redux"; import "./utils/i18n"; import camelCase from "camelcase";