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 &&
+
+ }
Welcome to Synapse Admin
-
);
@@ -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);
+ });
+});
+