Verified Commit 34de57b1 authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Rename login saga to session saga and create initialising sequence with splashscreen

parent 7602852f
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`login actions should create an action for a successful login 1`] = `
exports[`session actions should create an action for a successful login 1`] = `
Object {
"payload": Object {
"token": "token",
"username": "username",
},
"type": "LOGIN_SUCCESS",
"type": "SESSION_SUCCESS",
}
`;
exports[`login actions should create an action for a successful user profile load 1`] = `
exports[`session actions should create an action for a successful user profile load 1`] = `
Object {
"payload": Object {
"displayName": "displayName",
"photo": "photo",
},
"type": "LOGIN_PROFILE_SUCCESS",
"type": "SESSION_PROFILE_SUCCESS",
}
`;
exports[`login actions should create an action to load the user profile 1`] = `
exports[`session actions should create an action to init the session 1`] = `
Object {
"type": "SESSION_INIT",
}
`;
exports[`session actions should create an action to load the user profile 1`] = `
Object {
"payload": Object {
"token": "token",
},
"type": "LOGIN_PROFILE",
"type": "SESSION_PROFILE",
}
`;
exports[`login actions should create an action to log the user in 1`] = `
exports[`session actions should create an action to log the user in 1`] = `
Object {
"payload": Object {
"pass": "password",
"user": "username",
},
"type": "LOGIN_LOGIN",
"type": "SESSION_LOGIN",
}
`;
exports[`session actions should create an action to log the user out 1`] = `
Object {
"type": "SESSION_LOGOUT",
}
`;
exports[`login actions should create an action to log the user out 1`] = `
exports[`session actions should create an action to notify invalid token 1`] = `
Object {
"type": "LOGIN_LOGOUT",
"type": "SESSION_TOKEN_INVALID",
}
`;
import * as actions from '../../app/actions/login';
describe('login actions', () => {
it('should expose the login actions', () => {
expect(actions.SUCCESS).toEqual('LOGIN_SUCCESS');
expect(actions.LOGIN).toEqual('LOGIN_LOGIN');
expect(actions.LOGOUT).toEqual('LOGIN_LOGOUT');
expect(actions.PROFILE).toEqual('LOGIN_PROFILE');
expect(actions.PROFILE_SUCCESS).toEqual('LOGIN_PROFILE_SUCCESS');
import * as actions from '../../app/actions/session';
describe('session actions', () => {
it('should expose the session actions', () => {
expect(actions.INIT).toEqual('SESSION_INIT');
expect(actions.SUCCESS).toEqual('SESSION_SUCCESS');
expect(actions.LOGIN).toEqual('SESSION_LOGIN');
expect(actions.TOKEN_INVALID).toEqual('SESSION_TOKEN_INVALID');
expect(actions.LOGOUT).toEqual('SESSION_LOGOUT');
expect(actions.PROFILE).toEqual('SESSION_PROFILE');
expect(actions.PROFILE_SUCCESS).toEqual('SESSION_PROFILE_SUCCESS');
});
it('should create an action to init the session', () => {
expect(actions.init()).toMatchSnapshot();
});
it('should create an action to notify invalid token', () => {
expect(actions.tokenInvalid()).toMatchSnapshot();
});
it('should create an action to log the user in', () => {
......
......@@ -6,7 +6,7 @@ import * as deepLinkingActions from '../../app/actions/deepLinking';
import { url as siteURL, apiRequest, loggedInSelector } from '../../app/utils/url';
import * as navigationActions from '../../app/actions/navigation';
import * as eventActions from '../../app/actions/event';
import * as loginActions from '../../app/actions/login';
import * as loginActions from '../../app/actions/session';
import * as pizzaActions from '../../app/actions/pizza';
import { EVENT_LIST_SCENE } from '../../app/ui/components/navigator/scenes';
......
......@@ -5,9 +5,11 @@ import Snackbar from 'react-native-snackbar';
import { AsyncStorage } from 'react-native';
import { Sentry } from 'react-native-sentry';
import loginSaga, { DISPLAYNAMEKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY } from '../../app/sagas/login';
import sessionSaga, {
DISPLAYNAMEKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY
} from '../../app/sagas/session';
import { apiRequest } from '../../app/utils/url';
import * as loginActions from '../../app/actions/login';
import * as sessionActions from '../../app/actions/session';
import * as pushNotificationsActions from '../../app/actions/pushNotifications';
jest.mock('react-native-snackbar', () => ({
......@@ -35,48 +37,49 @@ jest.mock('react-native-sentry', () => ({
},
}));
describe('login saga', () => {
describe('session saga', () => {
const error = new Error('error');
describe('logging in', () => {
it('should show a snackbar on start', () => expectSaga(loginSaga)
.dispatch(loginActions.login('username', 'password'))
it('should show a snackbar on start', () => expectSaga(sessionSaga)
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(Snackbar.show).toBeCalledWith(
{ title: 'Logging in', duration: Snackbar.LENGTH_INDEFINITE });
{ title: 'Logging in', duration: Snackbar.LENGTH_INDEFINITE },
);
}));
it('should put the result data when the request succeeds', () => expectSaga(loginSaga)
it('should put the result data when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
])
.put(loginActions.success('username', 'abc123'))
.put(loginActions.profile('abc123'))
.dispatch(loginActions.login('username', 'password'))
.put(sessionActions.success('username', 'abc123'))
.put(sessionActions.profile('abc123'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun());
it('should show a snackbar when the request succeeds', () => expectSaga(loginSaga)
it('should show a snackbar when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
])
.dispatch(loginActions.login('username', 'password'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(Snackbar.dismiss).toBeCalled();
expect(Snackbar.show).toBeCalledWith(
{ title: 'Login successful' });
{ title: 'Login successful' },
);
}));
it('should save the token in the AsyncStorage when the request succeeds', () =>
expectSaga(loginSaga)
it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
])
.dispatch(loginActions.login('username', 'password'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(AsyncStorage.multiSet).toBeCalledWith([
......@@ -85,20 +88,21 @@ describe('login saga', () => {
]);
}));
it('should show a snackbar when the request fails', () => expectSaga(loginSaga)
it('should show a snackbar when the request fails', () => expectSaga(sessionSaga)
.provide([
[matchers.call.fn(apiRequest), throwError(error)],
])
.dispatch(loginActions.login('username', 'password'))
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(Snackbar.dismiss).toBeCalled();
expect(Snackbar.show).toBeCalledWith(
{ title: 'Login failed' });
{ title: 'Login failed' },
);
}));
it('should do a POST request', () => expectSaga(loginSaga)
.dispatch(loginActions.login('username', 'password'))
it('should do a POST request', () => expectSaga(sessionSaga)
.dispatch(sessionActions.login('username', 'password'))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('token-auth', {
......@@ -113,29 +117,30 @@ describe('login saga', () => {
});
describe('logging out', () => {
it('should remove the token from the AsyncStorage', () => expectSaga(loginSaga)
.dispatch(loginActions.logout())
it('should remove the token from the AsyncStorage', () => expectSaga(sessionSaga)
.dispatch(sessionActions.logout())
.silentRun()
.then(() => {
expect(AsyncStorage.multiRemove).toBeCalledWith([USERNAMEKEY, TOKENKEY]);
}));
it('should put a push notification invalidation action', () => expectSaga(loginSaga)
it('should put a push notification invalidation action', () => expectSaga(sessionSaga)
.put(pushNotificationsActions.invalidate())
.dispatch(loginActions.logout())
.dispatch(sessionActions.logout())
.silentRun());
it('should remove the token from the AsyncStorage', () => expectSaga(loginSaga)
.dispatch(loginActions.logout())
it('should remove the token from the AsyncStorage', () => expectSaga(sessionSaga)
.dispatch(sessionActions.logout())
.silentRun()
.then(() => {
expect(Snackbar.show).toBeCalledWith(
{ title: 'Logout successful' });
{ title: 'Logout successful' },
);
}));
});
describe('getting profile', () => {
it('should put the result data when the request succeeds', () => expectSaga(loginSaga)
it('should put the result data when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['members/me'] }), {
display_name: 'Johnny Test',
......@@ -144,11 +149,11 @@ describe('login saga', () => {
},
}],
])
.put(loginActions.profileSuccess('Johnny Test', 'http://example.org/photo.png'))
.dispatch(loginActions.profile('abc123'))
.put(sessionActions.profileSuccess('Johnny Test', 'http://example.org/photo.png'))
.dispatch(sessionActions.profile('abc123'))
.silentRun());
it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(loginSaga)
it('should save the token in the AsyncStorage when the request succeeds', () => expectSaga(sessionSaga)
.provide([
[matchers.call.like({ fn: apiRequest, args: ['members/me'] }), {
display_name: 'Johnny Test',
......@@ -157,7 +162,7 @@ describe('login saga', () => {
},
}],
])
.dispatch(loginActions.profile('abc123'))
.dispatch(sessionActions.profile('abc123'))
.silentRun()
.then(() => {
expect(AsyncStorage.multiSet).toBeCalledWith([
......@@ -166,15 +171,15 @@ describe('login saga', () => {
]);
}));
it('should not care about errors', () => expectSaga(loginSaga)
it('should not care about errors', () => expectSaga(sessionSaga)
.provide([
[matchers.call.fn(apiRequest), throwError(error)],
])
.dispatch(loginActions.profile('token'))
.dispatch(sessionActions.profile('token'))
.silentRun());
it('should do a GET request', () => expectSaga(loginSaga)
.dispatch(loginActions.profile('abc123'))
it('should do a GET request', () => expectSaga(sessionSaga)
.dispatch(sessionActions.profile('abc123'))
.silentRun()
.then(() => {
expect(apiRequest).toBeCalledWith('members/me', {
......
......@@ -4,12 +4,13 @@ import configureStore from 'redux-mock-store';
import renderer from 'react-test-renderer';
import ReduxNavigator from '../../../../app/ui/components/navigator/ReduxNavigator';
import reducer from '../../../../app/reducers';
import { LOGIN_SCENE } from '../../../../app/ui/components/navigator/scenes';
describe('ReduxNavigator component', () => {
const mockStore = configureStore(reducer);
const initialState = {
navigation: {
currentScene: 'home',
currentScene: LOGIN_SCENE,
previousScenes: [],
drawerOpen: false,
},
......@@ -18,8 +19,12 @@ describe('ReduxNavigator component', () => {
it('renders correctly', () => {
const tree = renderer
.create(<Provider store={store}><ReduxNavigator /></Provider>)
.create(
<Provider store={store}>
<ReduxNavigator />
</Provider>,
)
.toJSON();
expect(tree).toMatchSnapshot();
});
});
\ No newline at end of file
});
export const LOGIN = 'LOGIN_LOGIN';
export const SUCCESS = 'LOGIN_SUCCESS';
export const LOGOUT = 'LOGIN_LOGOUT';
export const TOKEN_INVALID = 'LOGIN_TOKEN_INVALID';
export const PROFILE = 'LOGIN_PROFILE';
export const PROFILE_SUCCESS = 'LOGIN_PROFILE_SUCCESS';
export const LOGIN = 'SESSION_LOGIN';
export const INIT = 'SESSION_INIT';
export const SUCCESS = 'SESSION_SUCCESS';
export const LOGOUT = 'SESSION_LOGOUT';
export const TOKEN_INVALID = 'SESSION_TOKEN_INVALID';
export const PROFILE = 'SESSION_PROFILE';
export const PROFILE_SUCCESS = 'SESSION_PROFILE_SUCCESS';
export function success(username, token) {
return { type: SUCCESS, payload: { username, token } };
......@@ -13,6 +14,10 @@ export function login(user, pass) {
return { type: LOGIN, payload: { user, pass } };
}
export function init() {
return { type: INIT };
}
export function logout() {
return { type: LOGOUT };
}
......
import React, { Component } from 'react';
import { AsyncStorage, Linking, Platform } from 'react-native';
import { Linking, Platform } from 'react-native';
import { applyMiddleware, createStore } from 'redux';
import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
......@@ -14,7 +14,7 @@ import reducers from './reducers';
import i18n from './utils/i18n';
import sagas from './sagas';
import ReduxNavigator from './ui/components/navigator/ReduxNavigator';
import * as loginActions from './actions/login';
import * as sessionActions from './actions/session';
import * as deepLinkingActions from './actions/deepLinking';
import { register } from './actions/pushNotifications';
......@@ -22,18 +22,6 @@ const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(sagas);
const USERNAMEKEY = '@MyStore:username';
const TOKENKEY = '@MyStore:token';
const DISPLAYNAMEKEY = '@MyStore:displayName';
const PHOTOKEY = '@MyStore:photo';
const PUSHCATEGORYKEY = '@MyStore:pushCategories';
const pairsToObject = (obj, pair) => {
const obj2 = { ...obj };
obj2[pair[0]] = pair[1];
return obj2;
};
FCM.on(FCMEvent.Notification, async (notif) => {
if (notif.fcm) {
FCM.presentLocalNotification({
......@@ -63,25 +51,7 @@ class Main extends Component {
}
componentDidMount() {
AsyncStorage.multiGet([USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY, PUSHCATEGORYKEY])
.then(
(result) => {
const values = result.reduce(pairsToObject, {});
const username = values[USERNAMEKEY];
const token = values[TOKENKEY];
const displayName = values[DISPLAYNAMEKEY];
const photo = values[PHOTOKEY];
const pushCategories = JSON.parse(values[PUSHCATEGORYKEY]);
if (username !== null && token !== null) {
store.dispatch(loginActions.success(username, token));
store.dispatch(loginActions.profileSuccess(displayName, photo));
store.dispatch(loginActions.profile(token));
store.dispatch(register(pushCategories));
}
},
);
store.dispatch(sessionActions.init());
this.addDeepLinkingHandler();
}
......
import * as navigationActions from '../actions/navigation';
import * as loginActions from '../actions/login';
import * as sessionActions from '../actions/session';
import { LOGIN_SCENE, SPLASH_SCENE } from '../ui/components/navigator/scenes';
const initialState = {
previousScenes: [],
currentScene: 'welcome',
currentScene: SPLASH_SCENE,
loggedIn: false,
drawerOpen: false,
};
......@@ -12,7 +13,7 @@ const initialState = {
export default function navigate(state = initialState, action = {}) {
const { currentScene, previousScenes, drawerOpen } = state;
switch (action.type) {
case loginActions.SUCCESS: {
case sessionActions.SUCCESS: {
return {
...state,
loggedIn: true,
......@@ -62,9 +63,12 @@ export default function navigate(state = initialState, action = {}) {
drawerOpen: action.payload.drawerOpen,
};
}
case loginActions.TOKEN_INVALID:
case loginActions.LOGOUT: {
return initialState;
case sessionActions.TOKEN_INVALID:
case sessionActions.LOGOUT: {
return {
...initialState,
currentScene: LOGIN_SCENE,
};
}
default:
return state;
......
import { defaultProfileImage } from '../utils/url';
import * as loginActions from '../actions/login';
import * as sessionActions from '../actions/session';
const initialState = {
token: '',
......@@ -11,20 +11,20 @@ const initialState = {
export default function session(state = initialState, action = {}) {
switch (action.type) {
case loginActions.SUCCESS:
case sessionActions.SUCCESS:
return {
...state,
username: action.payload.username,
token: action.payload.token,
};
case loginActions.PROFILE_SUCCESS:
case sessionActions.PROFILE_SUCCESS:
return {
...state,
displayName: action.payload.displayName,
photo: action.payload.photo,
};
case loginActions.TOKEN_INVALID:
case loginActions.LOGOUT:
case sessionActions.TOKEN_INVALID:
case sessionActions.LOGOUT:
return initialState;
default:
return state;
......
......@@ -5,7 +5,7 @@ import { url as siteURL, loggedInSelector } from '../utils/url';
import * as deepLinkingActions from '../actions/deepLinking';
import * as pizzaActions from '../actions/pizza';
import * as loginActions from '../actions/login';
import * as loginActions from '../actions/session';
import * as eventActions from '../actions/event';
import * as navigationActions from '../actions/navigation';
......
import { all, fork } from 'redux-saga/effects';
import loginSaga from './login';
import sessionSaga from './session';
import eventSaga from './event';
import profileSaga from './profile';
import welcomeSaga from './welcome';
......@@ -14,7 +14,7 @@ import settingsSaga from './settings';
const sagas = function* sagas() {
yield all([
fork(loginSaga),
fork(sessionSaga),
fork(eventSaga),
fork(profileSaga),
fork(welcomeSaga),
......
import { call, takeEvery, put } from 'redux-saga/effects';
import { call, put, takeEvery } from 'redux-saga/effects';
import { AsyncStorage } from 'react-native';
import Snackbar from 'react-native-snackbar';
import { Sentry } from 'react-native-sentry';
import { apiRequest } from '../utils/url';
import * as loginActions from '../actions/login';
import * as sessionActions from '../actions/session';
import * as pushNotificationsActions from '../actions/pushNotifications';
import { navigate } from '../actions/navigation';
import { LOGIN_SCENE, WELCOME_SCENE } from '../ui/components/navigator/scenes';
export const USERNAMEKEY = '@MyStore:username';
export const TOKENKEY = '@MyStore:token';
export const DISPLAYNAMEKEY = '@MyStore:displayName';
export const PHOTOKEY = '@MyStore:photo';
export const PUSHCATEGORYKEY = '@MyStore:pushCategories';
const login = function* login(action) {
const pairsToObject = (obj, pair) => {
const obj2 = { ...obj };
obj2[pair[0]] = pair[1];
return obj2;
};
const getStoredItems = () => AsyncStorage.multiGet([
USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY, PUSHCATEGORYKEY,
]);
function* init() {
try {
const result = yield call(getStoredItems);
const values = result.reduce(pairsToObject, {});
const username = values[USERNAMEKEY];
const token = values[TOKENKEY];
const displayName = values[DISPLAYNAMEKEY];
const photo = values[PHOTOKEY];
const pushCategories = JSON.parse(values[PUSHCATEGORYKEY]);
if (username !== null && token !== null) {
yield put(sessionActions.success(username, token));
yield put(sessionActions.profileSuccess(displayName, photo));
yield put(sessionActions.profile(token));
yield put(pushNotificationsActions.register(pushCategories));
} else {
yield put(navigate(LOGIN_SCENE, true));
}
} catch (e) {
Sentry.captureException(e);
}
}
function* login(action) {
const { user, pass } = action.payload;
Snackbar.show({ title: 'Logging in', duration: Snackbar.LENGTH_INDEFINITE });
......@@ -36,8 +73,8 @@ const login = function* login(action) {
[USERNAMEKEY, user],
[TOKENKEY, token],
]);
yield put(loginActions.success(user, token));
yield put(loginActions.profile(token));
yield put(sessionActions.success(user, token));
yield put(sessionActions.profile(token));
yield put(pushNotificationsActions.register());
Snackbar.dismiss();
Snackbar.show({ title: 'Login successful' });
......@@ -45,20 +82,20 @@ const login = function* login(action) {
Snackbar.dismiss();
Snackbar.show({ title: 'Login failed' });
}
};
}
const logout = function* logout() {
function* logout() {
yield call(AsyncStorage.multiRemove, [USERNAMEKEY, TOKENKEY]);
yield put(pushNotificationsActions.invalidate());
Snackbar.show({ title: 'Logout successful' });