Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Login to use redux-toolkit #2748

Merged
merged 10 commits into from
Nov 1, 2023
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"recaptcha",
"reportgenerator",
"sched",
"signup",
"sillsdev",
"Sldr",
"subtag",
Expand Down
2 changes: 1 addition & 1 deletion src/components/App/DefaultState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defaultState as goalTimelineState } from "components/GoalTimeline/DefaultState";
import { defaultState as loginState } from "components/Login/Redux/LoginReducer";
import { defaultState as loginState } from "components/Login/Redux/LoginReduxTypes";
import { defaultState as currentProjectState } from "components/Project/ProjectReduxTypes";
import { defaultState as exportProjectState } from "components/ProjectExport/Redux/ExportProjectReduxTypes";
import { defaultState as pronunciationsState } from "components/Pronunciations/Redux/PronunciationsReduxTypes";
Expand Down
8 changes: 4 additions & 4 deletions src/components/Login/LoginPage/LoginComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { BannerType } from "api/models";
import { getBannerText } from "backend";
import router from "browserRouter";
import { LoadingButton } from "components/Buttons";
import { LoginStatus } from "components/Login/Redux/LoginReduxTypes";
import { Path } from "types/path";
import { RuntimeConfig } from "types/runtimeConfig";
import theme from "types/theme";
Expand All @@ -34,8 +35,7 @@ export interface LoginDispatchProps {
}

export interface LoginStateProps {
loginAttempt?: boolean;
loginFailure?: boolean;
status: LoginStatus;
}

interface LoginProps
Expand Down Expand Up @@ -173,7 +173,7 @@ export class Login extends Component<LoginProps, LoginState> {
)}

{/* "Failed to log in" */}
{this.props.loginFailure && (
{this.props.status === LoginStatus.Failure && (
<Typography
variant="body2"
style={{ marginTop: 24, marginBottom: 24, color: "red" }}
Expand Down Expand Up @@ -229,7 +229,7 @@ export class Login extends Component<LoginProps, LoginState> {
color: "primary",
}}
disabled={!this.state.isVerified}
loading={this.props.loginAttempt}
loading={this.props.status === LoginStatus.InProgress}
>
{this.props.t("login.login")}
</LoadingButton>
Expand Down
9 changes: 3 additions & 6 deletions src/components/Login/LoginPage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,21 @@ import Login, {
LoginStateProps,
} from "components/Login/LoginPage/LoginComponent";
import {
asyncLogin,
asyncLogIn,
logoutAndResetStore,
} from "components/Login/Redux/LoginActions";
import { reset } from "rootActions";
import { StoreState } from "types";
import { StoreStateDispatch } from "types/Redux/actions";

function mapStateToProps(state: StoreState): LoginStateProps {
return {
loginAttempt: state.loginState && state.loginState.loginAttempt,
loginFailure: state.loginState && state.loginState.loginFailure,
};
return { status: state.loginState.loginStatus };
}

function mapDispatchToProps(dispatch: StoreStateDispatch): LoginDispatchProps {
return {
login: (username: string, password: string) => {
dispatch(asyncLogin(username, password));
dispatch(asyncLogIn(username, password));
},
logout: () => {
dispatch(logoutAndResetStore());
Expand Down
5 changes: 4 additions & 1 deletion src/components/Login/LoginPage/tests/LoginComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import "tests/reactI18nextMock";

import Login from "components/Login/LoginPage/LoginComponent";
import { LoginStatus } from "components/Login/Redux/LoginReduxTypes";

jest.mock(
"@matt-block/react-recaptcha-v2",
Expand All @@ -31,7 +32,9 @@ const MOCK_EVENT = { preventDefault: jest.fn(), target: { value: DATA } };
describe("Testing login component", () => {
beforeEach(async () => {
await act(async () => {
loginMaster = create(<Login logout={LOGOUT} reset={LOGOUT} />);
loginMaster = create(
<Login logout={LOGOUT} reset={LOGOUT} status={LoginStatus.Default} />
);
});
loginHandle = loginMaster.root.findByType(Login);
LOGOUT.mockClear();
Expand Down
98 changes: 45 additions & 53 deletions src/components/Login/Redux/LoginActions.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,65 @@
import { PayloadAction } from "@reduxjs/toolkit";
import Hex from "crypto-js/enc-hex";
import sha256 from "crypto-js/sha256";

import * as backend from "backend";
import router from "browserRouter";
import {
LoginActionTypes,
UserAction,
} from "components/Login/Redux/LoginReduxTypes";
setLoginAttemptAction,
setLoginFailureAction,
setLoginSuccessAction,
setSignupAttemptAction,
setSignupFailureAction,
setSignupSuccessAction,
} from "components/Login/Redux/LoginReducer";
import { reset } from "rootActions";
import { StoreStateDispatch } from "types/Redux/actions";
import { Path } from "types/path";
import { newUser } from "types/user";

// thunk action creator
export function asyncLogin(username: string, password: string) {
// Action Creation Functions

export function loginAttempt(username: string): PayloadAction {
return setLoginAttemptAction(username);
}

export function loginFailure(error: string): PayloadAction {
return setLoginFailureAction(error);
}

export function loginSuccess(): PayloadAction {
return setLoginSuccessAction();
}

export function signupAttempt(username: string): PayloadAction {
return setSignupAttemptAction(username);
}

export function signupFailure(error: string): PayloadAction {
return setSignupFailureAction(error);
}

export function signupSuccess(): PayloadAction {
return setSignupSuccessAction();
}

// Dispatch Functions

export function asyncLogIn(username: string, password: string) {
return async (dispatch: StoreStateDispatch) => {
dispatch(loginAttempt(username));
await backend
.authenticateUser(username, password)
.then(async (user) => {
dispatch(loginSuccess(user.username));
dispatch(loginSuccess());
// hash the user name and use it in analytics.identify
const analyticsId = Hex.stringify(sha256(user.id));
analytics.identify(analyticsId);
router.navigate(Path.ProjScreen);
})
.catch(() => dispatch(loginFailure(username)));
};
}

export function loginAttempt(username: string): UserAction {
return {
type: LoginActionTypes.LOGIN_ATTEMPT,
payload: { username },
};
}

export function loginFailure(username: string): UserAction {
return {
type: LoginActionTypes.LOGIN_FAILURE,
payload: { username },
};
}

export function loginSuccess(username: string): UserAction {
return {
type: LoginActionTypes.LOGIN_SUCCESS,
payload: { username },
.catch((err) =>
dispatch(loginFailure(err.response?.data ?? err.message))
);
};
}

Expand All @@ -63,41 +76,20 @@ export function asyncSignUp(
password: string
) {
return async (dispatch: StoreStateDispatch) => {
dispatch(signUpAttempt(username));
dispatch(signupAttempt(username));
// Create new user
const user = newUser(name, username, password);
user.email = email;
await backend
.addUser(user)
.then(() => {
dispatch(signUpSuccess(username));
dispatch(signupSuccess());
setTimeout(() => {
dispatch(asyncLogin(username, password));
dispatch(asyncLogIn(username, password));
}, 1000);
})
.catch((err) =>
dispatch(signUpFailure(err.response?.data ?? err.message))
dispatch(signupFailure(err.response?.data ?? err.message))
);
};
}

export function signUpAttempt(username: string): UserAction {
return {
type: LoginActionTypes.SIGN_UP_ATTEMPT,
payload: { username },
};
}

export function signUpFailure(errorMessage: string): UserAction {
return {
type: LoginActionTypes.SIGN_UP_FAILURE,
payload: { username: errorMessage },
};
}

export function signUpSuccess(username: string): UserAction {
return {
type: LoginActionTypes.SIGN_UP_SUCCESS,
payload: { username },
};
}
117 changes: 49 additions & 68 deletions src/components/Login/Redux/LoginReducer.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,53 @@
import { createSlice } from "@reduxjs/toolkit";

import {
LoginActionTypes,
LoginState,
UserAction,
LoginStatus,
defaultState,
} from "components/Login/Redux/LoginReduxTypes";
import { StoreAction, StoreActionTypes } from "rootActions";
import { StoreActionTypes } from "rootActions";

const loginSlice = createSlice({
name: "loginState",
initialState: defaultState,
reducers: {
setLoginAttemptAction: (state, action) => {
state.error = "";
state.loginStatus = LoginStatus.InProgress;
state.signupStatus = LoginStatus.Default;
state.username = action.payload;
},
setLoginFailureAction: (state, action) => {
state.error = action.payload;
state.loginStatus = LoginStatus.Failure;
},
setLoginSuccessAction: (state) => {
state.loginStatus = LoginStatus.Success;
},
setSignupAttemptAction: (state, action) => {
state.error = "";
state.loginStatus = LoginStatus.Default;
state.signupStatus = LoginStatus.InProgress;
state.username = action.payload;
},
setSignupFailureAction: (state, action) => {
state.error = action.payload;
state.signupStatus = LoginStatus.Failure;
},
setSignupSuccessAction: (state) => {
state.signupStatus = LoginStatus.Success;
},
},
extraReducers: (builder) =>
builder.addCase(StoreActionTypes.RESET, () => defaultState),
});

export const defaultState: LoginState = {
username: "",
loginAttempt: false,
loginFailure: false,
loginSuccess: false,
signUpAttempt: false,
signUpFailure: "",
signUpSuccess: false,
};
export const {
setLoginAttemptAction,
setLoginFailureAction,
setLoginSuccessAction,
setSignupAttemptAction,
setSignupFailureAction,
setSignupSuccessAction,
} = loginSlice.actions;

export const loginReducer = (
state: LoginState = defaultState, //createStore() calls each reducer with undefined state
action: StoreAction | UserAction
): LoginState => {
switch (action.type) {
case LoginActionTypes.LOGIN_ATTEMPT:
return {
...state,
username: action.payload.username,
loginAttempt: true,
loginSuccess: false,
loginFailure: false,
};
case LoginActionTypes.LOGIN_FAILURE:
return {
...state,
username: action.payload.username,
loginAttempt: false,
loginFailure: true,
loginSuccess: false,
};
case LoginActionTypes.LOGIN_SUCCESS:
return {
...state,
username: action.payload.username,
loginSuccess: true,
};
case LoginActionTypes.SIGN_UP_ATTEMPT:
return {
...state,
username: action.payload.username,
signUpAttempt: true,
signUpFailure: "",
signUpSuccess: false,
};
case LoginActionTypes.SIGN_UP_SUCCESS:
return {
...state,
username: action.payload.username,
signUpAttempt: false,
signUpSuccess: true,
};
case LoginActionTypes.SIGN_UP_FAILURE:
return {
...state,
signUpAttempt: false,
signUpFailure: action.payload.username,
signUpSuccess: false,
};
case StoreActionTypes.RESET:
return defaultState;
default:
return state;
}
};
export default loginSlice.reducer;
Loading