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