Skip to content

Commit

Permalink
Merge pull request #39 from nickovchinnikov/userSlice
Browse files Browse the repository at this point in the history
User slice development
  • Loading branch information
nickovchinnikov authored May 4, 2022
2 parents caa7717 + 7a5d987 commit bbd8c90
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 2 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,9 @@
### User slice

[pull request](https://github.com/nickovchinnikov/coursesbox/pull/38)

### Async actions

### Async actions tests

[pull request](https://github.com/nickovchinnikov/coursesbox/pull/39)
1 change: 1 addition & 0 deletions frontend/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_STRAPI_API_URL=http://localhost:1337/api
67 changes: 66 additions & 1 deletion frontend/services/userSlice.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { mockUser } from "@/mocks/user";

import { reducer, actions, initialState } from "./userSlice";
import { reducer, actions, initialState, login } from "./userSlice";

const updatedState = {
jwt: mockUser.jwt,
username: mockUser.user.username,
email: mockUser.user.email,
};

const loginData = {
identifier: mockUser.user.email,
password: mockUser.user.password,
};

const requestId = "someid";

describe("User slice check", () => {
describe("Update state actions", () => {
it("should update the full state", () => {
Expand Down Expand Up @@ -41,4 +48,62 @@ describe("User slice check", () => {
);
});
});

describe("Login state flow", () => {
it("should set the request state to pending", () => {
expect(
reducer(
{
...initialState,
error: {
message: "Rejected",
},
},
login.pending(requestId, loginData)
)
).toEqual({
...initialState,
requestState: "pending",
});
});
it("should set the request state to fulfilled and reset any previous errors", () => {
expect(
reducer(
{
...initialState,
error: {
message: "Rejected",
},
},
login.fulfilled(
{
jwt: updatedState.jwt,
user: {
username: updatedState.username,
email: updatedState.email,
},
},
requestId,
loginData
)
)
).toEqual({
...updatedState,
requestState: "fulfilled",
});
});
it("should set the request state to rejected", () => {
const payloadError = { error: { name: "500", message: "Server error" } };
expect(
reducer(
initialState,
login.rejected({} as Error, requestId, loginData, payloadError)
)
).toEqual({
...initialState,
error: payloadError.error,
requestState: "rejected",
});
});
});
});
89 changes: 88 additions & 1 deletion frontend/services/userSlice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import {
createSlice,
PayloadAction,
createAsyncThunk,
SerializedError,
} from "@reduxjs/toolkit";

type RequestState = "pending" | "fulfilled" | "rejected";

export type UserState = {
jwt: string;
username: string;
email: string;
requestState?: RequestState;
error?: SerializedError;
};

export type LoginData = {
identifier?: string;
password?: string;
};

type UserPayload = { jwt: string; user: { username: string; email: string } };

export const initialState: UserState = {
jwt: "",
username: "",
Expand All @@ -22,6 +38,77 @@ export const userSlice = createSlice({
}),
clear: () => initialState,
},
extraReducers: (builder) => {
builder
.addCase(login.fulfilled, (state, { payload }) => {
state.requestState = "fulfilled";
state.jwt = payload.jwt;
state.username = payload.user.username;
state.email = payload.user.email;
state.error = undefined;
})
.addCase(login.pending, (state) => {
state.requestState = "pending";
state.error = undefined;
})
.addCase(login.rejected, (state, { payload }) => {
state.requestState = "rejected";
const payloadError = (payload as { error: SerializedError })?.error;
state.error = payloadError;
});
},
});

export const { actions, reducer } = userSlice;

const api_url = process.env.NEXT_PUBLIC_STRAPI_API_URL;

const clearUserInfoFromLocalStorage = () => {
localStorage.removeItem("jwt");
localStorage.removeItem("username");
localStorage.removeItem("email");
};

const setupUserInfoToLocalStorage = (result: UserPayload) => {
localStorage.setItem("jwt", result.jwt);
localStorage.setItem("username", result?.user?.username);
localStorage.setItem("email", result?.user?.email);
};

export const login = createAsyncThunk<UserPayload, LoginData>(
"user/login",
async (loginData, { rejectWithValue }) => {
try {
const jwt = localStorage.getItem("jwt");
const response = jwt
? await fetch(`${api_url}/users/me`, {
method: "GET",
headers: {
Authorization: `Bearer ${jwt}`,
},
})
: await fetch(`${api_url}/auth/local`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(loginData),
});

const data = await response.json();

if (response.status < 200 || response.status >= 300) {
clearUserInfoFromLocalStorage();
return rejectWithValue(data);
}

const result = (jwt ? { jwt, user: data } : data) as UserPayload;

setupUserInfoToLocalStorage(result);
return result;
} catch (error) {
clearUserInfoFromLocalStorage();
return rejectWithValue(error);
}
}
);

1 comment on commit bbd8c90

@vercel
Copy link

@vercel vercel bot commented on bbd8c90 May 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.