Skip to content

Commit

Permalink
Extracting user from localStorage (#852)
Browse files Browse the repository at this point in the history
closes #743

The idea is to intercept editor actions (i.e. actions that could call
the API) and update the user object in state prior to that call being
made. That way if the user object changes for whatever reason (like cred
renewal) the latest user object will be used for the API call
  • Loading branch information
sra405 authored Dec 21, 2023
1 parent 5de17cd commit b7c206f
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 3 additions & 14 deletions src/containers/WebComponentLoader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
36 changes: 22 additions & 14 deletions src/containers/WebComponentLoader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => ({
Expand All @@ -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: {
Expand All @@ -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(
<Provider store={store}>
Expand Down Expand Up @@ -87,7 +88,7 @@ describe("When loaded as a cold user", () => {
},
hasShownSavePrompt: false,
justLoaded: false,
user: null,
user: undefined,
saveTriggered: false,
});
});
Expand All @@ -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(
<Provider store={store}>
<CookiesProvider cookies={cookies}>
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -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(
Expand All @@ -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(
<Provider store={store}>
Expand Down Expand Up @@ -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(
<Provider store={store}>
Expand Down
18 changes: 18 additions & 0 deletions src/redux/middlewares/localStorageUserMiddleware.js
Original file line number Diff line number Diff line change
@@ -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;
94 changes: 94 additions & 0 deletions src/redux/middlewares/localStorageUserMiddleware.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -18,7 +19,7 @@ const store = configureStore({
],
ignoredPaths: ["auth.user"],
},
}),
}).concat(userMiddleWare(setUser)),
});

export default store;
2 changes: 1 addition & 1 deletion src/web-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down

0 comments on commit b7c206f

Please sign in to comment.