Add logout to the topnav (#45)

* WIP

* Remove session token after logout.

* Add logout reducer test

* Pop stack to login

* Use newProps and doFetchWithResponse

* Setup the reducers to clear the store
This commit is contained in:
Mike Piccolo 2016-10-20 11:49:03 -07:00 committed by Thomas Hopkins
parent bfec2fedc9
commit 25f284a068
15 changed files with 208 additions and 2 deletions

View file

@ -36,7 +36,6 @@ export function bindClientFunc(clientFunc, request, success, failure, ...args) {
try {
const data = await clientFunc(...args);
dispatch(requestSuccess(success, data), getState);
} catch (err) {
dispatch(requestFailure(failure, err));

15
src/actions/logout.js Normal file
View file

@ -0,0 +1,15 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {LogoutTypes as types} from 'constants';
import Client from 'client/client_instance';
import {bindClientFunc} from 'actions/helpers.js';
export function logout() {
return bindClientFunc(
Client.logout,
types.LOGOUT_REQUEST,
types.LOGOUT_SUCCESS,
types.LOGOUT_FAILURE
);
}

View file

@ -182,6 +182,17 @@ export default class Client {
return data;
}
logout = async () => {
const response = await this.doFetchWithResponse(
`${this.getUsersRoute()}/logout`,
{method: 'post'}
);
if (response.ok) {
this.token = '';
}
return response;
}
getInitialLoad = async () => {
return this.doFetch(
`${this.getUsersRoute()}/initial_load`,

49
src/components/logout.js Normal file
View file

@ -0,0 +1,49 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import {TouchableHighlight, Text} from 'react-native';
import * as logoutActions from 'actions/logout';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {Actions as Routes} from 'react-native-router-flux';
const propTypes = {
logout: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
class Logout extends Component {
static propTypes = propTypes;
componentWillReceiveProps(newProps) {
if (this.props.logout.status === 'fetching' &&
newProps.logout.status === 'fetched') {
Routes.popTo('goToLogin');
}
}
logout = () => this.props.actions.logout();
render() {
return (
<TouchableHighlight onPress={this.logout}>
<Text>{'logout'}</Text>
</TouchableHighlight>
);
}
}
function mapStateToProps(state) {
return {
logout: state.views.logout
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(logoutActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Logout);

View file

@ -2,6 +2,7 @@ import * as ChannelsTypes from './channels';
import * as DeviceTypes from './device';
import * as GeneralTypes from './general';
import * as LoginTypes from './login';
import * as LogoutTypes from './logout';
import * as TeamsTypes from './teams';
import * as PostsTypes from './posts';
@ -9,6 +10,7 @@ export {
DeviceTypes,
GeneralTypes,
LoginTypes,
LogoutTypes,
TeamsTypes,
ChannelsTypes,
PostsTypes

3
src/constants/logout.js Normal file
View file

@ -0,0 +1,3 @@
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';

View file

@ -37,6 +37,8 @@ export default function reduceChannels(state = initState, action) {
error: action.error
};
case types.LOGOUT_SUCCESS:
return initState;
default:
return state;
}

View file

@ -7,6 +7,7 @@ import channels from './channels';
import device from './device.js';
import general from './general.js';
import login from './login';
import logout from './logout';
import teams from './teams';
import posts from './posts';
@ -19,7 +20,8 @@ const entities = combineReducers({
const views = combineReducers({
device,
login
login,
logout
});
export default combineReducers({

View file

@ -26,6 +26,8 @@ export default function reduceLogin(state = initState, action) {
error: action.error
};
case types.LOGOUT_SUCCESS:
return initState;
default:
return state;
}

31
src/reducers/logout.js Normal file
View file

@ -0,0 +1,31 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {LogoutTypes as types} from 'constants';
export const initState = {
status: 'not fetched',
error: null
};
export default function reduceLogout(state = initState, action) {
switch (action.type) {
case types.LOGOUT_REQUEST:
return {...state,
status: 'fetching',
error: null
};
case types.LOGOUT_SUCCESS:
return {...state,
status: 'fetched'
};
case types.LOGOUT_FAILURE:
return {...state,
status: 'failed',
error: action.error
};
default:
return state;
}
}

View file

@ -25,6 +25,8 @@ export default function reducePosts(state = initState, action) {
error: action.error
};
case types.LOGOUT_SUCCESS:
return initState;
default:
return state;
}

View file

@ -31,6 +31,8 @@ export default function reduceTeams(state = initState, action) {
error: action.error
};
case types.LOGOUT_SUCCESS:
return initState;
default:
return state;
}

View file

@ -10,6 +10,8 @@ import LoginContainer from 'containers/login_container';
import SelectServerContainer from 'containers/select_server_container';
import SelectTeamContainer from 'containers/select_team_container';
import PostsListContainer from 'containers/posts_list_container';
import Logout from 'components/logout';
import * as logout from 'actions/logout';
import {injectIntl, intlShape} from 'react-intl';
@ -39,16 +41,25 @@ class Routes extends Component {
key='goToChannelsList'
component={ChannelsListContainer}
title={formatMessage({id: 'routes.channels', defaultMessage: 'Channels'})}
renderRightButton={() =>
<Logout actions={logout}/>
}
/>
<Scene
key='goToSelectTeam'
component={SelectTeamContainer}
title={formatMessage({id: 'routes.selectTeam', defaultMessage: 'Select Team'})}
renderRightButton={() =>
<Logout actions={logout}/>
}
/>
<Scene
key='goToPostsList'
component={PostsListContainer}
title={formatMessage({id: 'routes.postsList', defaultMessage: 'Posts List'})}
renderRightButton={() =>
<Logout actions={logout}/>
}
/>
</Scene>
</Router>

View file

@ -0,0 +1,75 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import assert from 'assert';
import reduceLogout, {initState} from 'reducers/logout';
import {LogoutTypes as types} from 'constants';
describe('logout reducer', () => {
describe('Init', () => {
let store;
let expectedStore;
before(() => {
store = reduceLogout(store, {type: ''});
expectedStore = {...initState};
});
it('should be initial state', () => {
assert.equal(typeof store, 'object');
});
it('have a specifc initial state', () => {
assert.deepEqual(store, expectedStore);
});
});
describe(`when ${types.LOGOUT_REQUEST}`, () => {
let store;
let expectedStore;
before(() => {
store = reduceLogout(store, {
type: types.LOGOUT_REQUEST
});
expectedStore = {
...initState,
status: 'fetching'
};
});
it('should set status to fetching', () => {
assert.deepEqual(store, expectedStore);
});
});
describe(`when ${types.LOGOUT_SUCCESS}`, () => {
let store;
let expectedStore;
before(() => {
store = reduceLogout(store, {
type: types.LOGOUT_SUCCESS
});
expectedStore = {
...initState,
status: 'fetched'
};
});
it('should set status to fetched and data', () => {
assert.deepEqual(store, expectedStore);
});
});
describe(`when ${types.LOGOUT_FAILURE}`, () => {
let store;
let error;
let expectedStore;
before(() => {
error = {id: 'the.error.id', message: 'Something went wrong'};
store = reduceLogout(store, {
type: types.LOGOUT_FAILURE,
error
});
expectedStore = {
...initState,
status: 'failed',
error
};
});
it('should set status to failed and error', () => {
assert.deepEqual(store, expectedStore);
});
});
});