diff --git a/src/features/common/redux/initialState.js b/src/features/common/redux/initialState.js index 2dd2879..7258b08 100644 --- a/src/features/common/redux/initialState.js +++ b/src/features/common/redux/initialState.js @@ -7,6 +7,7 @@ // NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions. const initialState = { + mtx: null, }; export default initialState; diff --git a/src/features/home/Login.js b/src/features/home/Login.js index 61923c7..d516072 100644 --- a/src/features/home/Login.js +++ b/src/features/home/Login.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; +import { Redirect } from 'react-router-dom' import matrixLogo from '../../images/matrix-logo.svg'; import * as actions from './redux/actions'; @@ -12,25 +12,96 @@ export class Login extends Component { actions: PropTypes.object.isRequired, }; + constructor(props) { + super(props); + + this.state = { + homeserver: 'https://matrix.org', + username: '', + password: '', + submitted: false + }; + + this.handleChange = this.handleChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + handleChange(e) { + const { name, value } = e.target; + this.setState({ [name]: value }); + } + + handleSubmit(e) { + e.preventDefault(); + + this.setState({ submitted: true }); + const { homeserver, username, password } = this.state; + const { login } = this.props.actions; + const { history } = this.props; + if (homeserver && username && password) { + login(homeserver, username, password).then( + history.push("/user-admin/list") + ); + } + } + render() { + const { mtx } = this.props; + const { loginPending } = this.props.home; + const { homeserver, username, password, submitted } = this.state; return (
+ {mtx && mtx.clientRunning && + + }
logo

Welcome to Synapse Admin

-
+ - + + - + + + + + +
Username: + + + + {submitted && !homeserver && +
Homeserver is required
+ } +
Password: + + + + {submitted && !username && +
Username is required
+ } +
+ + + + {submitted && !password && +
Password is required
+ } +
- Login +
+ + {loginPending && + logging in + } +
); @@ -40,6 +111,7 @@ export class Login extends Component { /* istanbul ignore next */ function mapStateToProps(state) { return { + mtx: state.common.mtx, home: state.home, }; } diff --git a/src/features/home/redux/actions.js b/src/features/home/redux/actions.js index e69de29..1874ace 100644 --- a/src/features/home/redux/actions.js +++ b/src/features/home/redux/actions.js @@ -0,0 +1 @@ +export { login, dismissLoginError } from './login'; diff --git a/src/features/home/redux/constants.js b/src/features/home/redux/constants.js index e69de29..160f5a9 100644 --- a/src/features/home/redux/constants.js +++ b/src/features/home/redux/constants.js @@ -0,0 +1,4 @@ +export const HOME_LOGIN_BEGIN = 'HOME_LOGIN_BEGIN'; +export const HOME_LOGIN_SUCCESS = 'HOME_LOGIN_SUCCESS'; +export const HOME_LOGIN_FAILURE = 'HOME_LOGIN_FAILURE'; +export const HOME_LOGIN_DISMISS_ERROR = 'HOME_LOGIN_DISMISS_ERROR'; diff --git a/src/features/home/redux/initialState.js b/src/features/home/redux/initialState.js index fd94220..18d28e5 100644 --- a/src/features/home/redux/initialState.js +++ b/src/features/home/redux/initialState.js @@ -1,4 +1,6 @@ const initialState = { + loginPending: false, + loginError: null, }; export default initialState; diff --git a/src/features/home/redux/login.js b/src/features/home/redux/login.js new file mode 100644 index 0000000..4399ebc --- /dev/null +++ b/src/features/home/redux/login.js @@ -0,0 +1,91 @@ +import { + HOME_LOGIN_BEGIN, + HOME_LOGIN_SUCCESS, + HOME_LOGIN_FAILURE, + HOME_LOGIN_DISMISS_ERROR, +} from './constants'; +import Matrix from 'matrix-js-sdk'; + +export function login(homeserver, username, password) { + return (dispatch, getState) => { + dispatch({ + type: HOME_LOGIN_BEGIN, + }); + + // Return a promise so that you could control UI flow without states in the store. + // For example: after submit a form, you need to redirect the page to another when succeeds or show some errors message if fails. + // It's hard to use state to manage it, but returning a promise allows you to easily achieve it. + // e.g.: handleSubmit() { this.props.actions.submitForm(data).then(()=> {}).catch(() => {}); } + const promise = new Promise((resolve, reject) => { + var state = getState(); + state.common.mtx = Matrix.createClient(homeserver); + const doRequest = state.common.mtx.login("m.login.password", { "user": username, "password": password }); + doRequest.then( + (res) => { + state.common.mtx.startClient(); + dispatch({ + type: HOME_LOGIN_SUCCESS, + data: res, + }); + resolve(res); + }, + // Use rejectHandler as the second argument so that render errors won't be caught. + (err) => { + dispatch({ + type: HOME_LOGIN_FAILURE, + data: { error: err }, + }); + reject(err.message); + }, + ); + }); + + return promise; + }; +} + +// Async action saves request error by default, this method is used to dismiss the error info. +// If you don't want errors to be saved in Redux store, just ignore this method. +export function dismissLoginError() { + return { + type: HOME_LOGIN_DISMISS_ERROR, + }; +} + +export function reducer(state, action) { + switch (action.type) { + case HOME_LOGIN_BEGIN: + // Just after a request is sent + return { + ...state, + loginPending: true, + loginError: null, + }; + + case HOME_LOGIN_SUCCESS: + // The request is success + return { + ...state, + loginPending: false, + loginError: null, + }; + + case HOME_LOGIN_FAILURE: + // The request is failed + return { + ...state, + loginPending: false, + loginError: action.data.error, + }; + + case HOME_LOGIN_DISMISS_ERROR: + // Dismiss the request failure error + return { + ...state, + loginError: null, + }; + + default: + return state; + } +} diff --git a/src/features/home/redux/reducer.js b/src/features/home/redux/reducer.js index 595a980..bd92c92 100644 --- a/src/features/home/redux/reducer.js +++ b/src/features/home/redux/reducer.js @@ -1,6 +1,8 @@ import initialState from './initialState'; +import { reducer as loginReducer } from './login'; const reducers = [ + loginReducer, ]; export default function reducer(state = initialState, action) { diff --git a/tests/features/home/redux/login.test.js b/tests/features/home/redux/login.test.js new file mode 100644 index 0000000..808078d --- /dev/null +++ b/tests/features/home/redux/login.test.js @@ -0,0 +1,97 @@ +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import nock from 'nock'; + +import { + HOME_LOGIN_BEGIN, + HOME_LOGIN_SUCCESS, + HOME_LOGIN_FAILURE, + HOME_LOGIN_DISMISS_ERROR, +} from '../../../../src/features/home/redux/constants'; + +import { + login, + dismissLoginError, + reducer, +} from '../../../../src/features/home/redux/login'; + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('home/redux/login', () => { + afterEach(() => { + nock.cleanAll(); + }); + + it('dispatches success action when login succeeds', () => { + const store = mockStore({}); + + return store.dispatch(login()) + .then(() => { + const actions = store.getActions(); + expect(actions[0]).toHaveProperty('type', HOME_LOGIN_BEGIN); + expect(actions[1]).toHaveProperty('type', HOME_LOGIN_SUCCESS); + }); + }); + + it('dispatches failure action when login fails', () => { + const store = mockStore({}); + + return store.dispatch(login({ error: true })) + .catch(() => { + const actions = store.getActions(); + expect(actions[0]).toHaveProperty('type', HOME_LOGIN_BEGIN); + expect(actions[1]).toHaveProperty('type', HOME_LOGIN_FAILURE); + expect(actions[1]).toHaveProperty('data.error', expect.anything()); + }); + }); + + it('returns correct action by dismissLoginError', () => { + const expectedAction = { + type: HOME_LOGIN_DISMISS_ERROR, + }; + expect(dismissLoginError()).toEqual(expectedAction); + }); + + it('handles action type HOME_LOGIN_BEGIN correctly', () => { + const prevState = { loginPending: false }; + const state = reducer( + prevState, + { type: HOME_LOGIN_BEGIN } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.loginPending).toBe(true); + }); + + it('handles action type HOME_LOGIN_SUCCESS correctly', () => { + const prevState = { loginPending: true }; + const state = reducer( + prevState, + { type: HOME_LOGIN_SUCCESS, data: {} } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.loginPending).toBe(false); + }); + + it('handles action type HOME_LOGIN_FAILURE correctly', () => { + const prevState = { loginPending: true }; + const state = reducer( + prevState, + { type: HOME_LOGIN_FAILURE, data: { error: new Error('some error') } } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.loginPending).toBe(false); + expect(state.loginError).toEqual(expect.anything()); + }); + + it('handles action type HOME_LOGIN_DISMISS_ERROR correctly', () => { + const prevState = { loginError: new Error('some error') }; + const state = reducer( + prevState, + { type: HOME_LOGIN_DISMISS_ERROR } + ); + expect(state).not.toBe(prevState); // should be immutable + expect(state.loginError).toBe(null); + }); +}); +