From bf89409f60228fef8c59cc5f7f5f3d197a90d535 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 23 Aug 2017 19:53:06 +0700 Subject: [PATCH] Remove global dependency on Storage --- client/coral-admin/src/actions/auth.js | 28 +++--- client/coral-admin/src/actions/moderation.js | 8 +- client/coral-admin/src/index.js | 10 ++ client/coral-admin/src/reducers/moderation.js | 7 +- .../Dashboard/components/CountdownTimer.js | 25 +++-- client/coral-embed-stream/src/actions/auth.js | 26 ++++-- .../components/TalkProvider.js | 2 + client/coral-framework/helpers/storage.js | 92 ------------------- client/coral-framework/services/bootstrap.js | 12 ++- client/coral-framework/services/storage.js | 39 ++++++++ 10 files changed, 117 insertions(+), 132 deletions(-) delete mode 100644 client/coral-framework/helpers/storage.js create mode 100644 client/coral-framework/services/storage.js diff --git a/client/coral-admin/src/actions/auth.js b/client/coral-admin/src/actions/auth.js index 3433fc95a..422258f50 100644 --- a/client/coral-admin/src/actions/auth.js +++ b/client/coral-admin/src/actions/auth.js @@ -1,6 +1,5 @@ import bowser from 'bowser'; import * as actions from '../constants/auth'; -import * as Storage from 'coral-framework/helpers/storage'; import t from 'coral-framework/services/i18n'; import jwtDecode from 'jwt-decode'; @@ -8,7 +7,7 @@ import jwtDecode from 'jwt-decode'; // SIGN IN //============================================================================== -export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client}) => { +export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, {rest, client, storage}) => { dispatch({type: actions.LOGIN_REQUEST}); const params = { @@ -29,8 +28,8 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch, _, .then(({user, token}) => { if (!user) { - if (!bowser.safari && !bowser.ios) { - Storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && storage) { + storage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -114,13 +113,13 @@ const checkLoginFailure = (error) => ({ error }); -export const checkLogin = () => (dispatch, _, {rest, client}) => { +export const checkLogin = () => (dispatch, _, {rest, client, storage}) => { dispatch(checkLoginRequest()); return rest('/auth') .then(({user}) => { if (!user) { - if (!bowser.safari && !bowser.ios) { - Storage.removeItem('token'); + if (!bowser.safari && !bowser.ios && storage) { + storage.removeItem('token'); } return dispatch(checkLoginFailure('not logged in')); } @@ -139,9 +138,11 @@ export const checkLogin = () => (dispatch, _, {rest, client}) => { // LOGOUT //============================================================================== -export const logout = () => (dispatch, _, {rest, client}) => { +export const logout = () => (dispatch, _, {rest, client, storage}) => { return rest('/auth', {method: 'DELETE'}).then(() => { - Storage.removeItem('token'); + if (storage) { + storage.removeItem('token'); + } // Reset the websocket. client.resetWebsocket(); @@ -154,10 +155,11 @@ export const logout = () => (dispatch, _, {rest, client}) => { // AUTH TOKEN //============================================================================== -export const handleAuthToken = (token) => (dispatch) => { - Storage.setItem('exp', jwtDecode(token).exp); - Storage.setItem('token', token); - +export const handleAuthToken = (token) => (dispatch, _, {storage}) => { + if (storage) { + storage.setItem('exp', jwtDecode(token).exp); + storage.setItem('token', token); + } dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; diff --git a/client/coral-admin/src/actions/moderation.js b/client/coral-admin/src/actions/moderation.js index 5136db4c3..a3ff2ca3e 100644 --- a/client/coral-admin/src/actions/moderation.js +++ b/client/coral-admin/src/actions/moderation.js @@ -4,15 +4,17 @@ export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open}); export const singleView = () => ({type: actions.SINGLE_VIEW}); // hide shortcuts note -export const hideShortcutsNote = () => { +export const hideShortcutsNote = () => (dispatch, _, {storage}) => { try { - window.localStorage.setItem('coral:shortcutsNote', 'hide'); + if (storage) { + storage.setItem('coral:shortcutsNote', 'hide'); + } } catch (e) { // above will fail in Safari private mode } - return {type: actions.HIDE_SHORTCUTS_NOTE}; + dispatch({type: actions.HIDE_SHORTCUTS_NOTE}); }; export const setSortOrder = (order) => ({ diff --git a/client/coral-admin/src/index.js b/client/coral-admin/src/index.js index 2b0a982b4..5df2ce391 100644 --- a/client/coral-admin/src/index.js +++ b/client/coral-admin/src/index.js @@ -10,12 +10,22 @@ import graphqlExtension from './graphql'; import pluginsConfig from 'pluginsConfig'; import {toast} from 'react-toastify'; import {createNotificationService} from './services/notification'; +import {hideShortcutsNote} from './actions/moderation'; + +function hidrateStore({store, storage}) { + if (storage && storage.getItem('coral:shortcutsNote') === 'hide') { + store.dispatch(hideShortcutsNote()); + } +} smoothscroll.polyfill(); const notification = createNotificationService(toast); const context = createContext({reducers, graphqlExtension, pluginsConfig, notification}); +// hidrate Store with external data. +hidrateStore(context); + render( diff --git a/client/coral-admin/src/reducers/moderation.js b/client/coral-admin/src/reducers/moderation.js index e0a608488..3a4a3f5e9 100644 --- a/client/coral-admin/src/reducers/moderation.js +++ b/client/coral-admin/src/reducers/moderation.js @@ -5,14 +5,17 @@ const initialState = { modalOpen: false, storySearchVisible: false, storySearchString: '', - shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show', + shortcutsNoteVisible: 'show', sortOrder: 'REVERSE_CHRONOLOGICAL', }; export default function moderation (state = initialState, action) { switch (action.type) { case actions.MODERATION_CLEAR_STATE: - return initialState; + return { + ...initialState, + shortcutsNoteVisible: state.shortcutsNoteVisible, + }; case actions.TOGGLE_MODAL: return { ...state, diff --git a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js index 9e1995b7f..6864acb6b 100644 --- a/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js +++ b/client/coral-admin/src/routes/Dashboard/components/CountdownTimer.js @@ -5,17 +5,24 @@ import {Icon} from 'coral-ui'; import t from 'coral-framework/services/i18n'; const refreshIntervalSeconds = 60 * 5; +// TODO: refactor out storage code into redux. + class CountdownTimer extends React.Component { + static contextTypes = { + storage: PropTypes.object, + }; + static propTypes = { handleTimeout: PropTypes.func.isRequired } - constructor (props) { - super(props); + constructor (props, context) { + super(props, context); + const {storage} = context; try { - if (window.localStorage.getItem('coral:dashboardNote') === null) { - window.localStorage.setItem('coral:dashboardNote', 'show'); + if (storage && storage.getItem('coral:dashboardNote') === null) { + storage.setItem('coral:dashboardNote', 'show'); } } catch (e) { @@ -24,7 +31,7 @@ class CountdownTimer extends React.Component { this.state = { secondsUntilRefresh: refreshIntervalSeconds, - dashboardNote: window.localStorage.getItem('coral:dashboardNote') || 'show' + dashboardNote: (storage && storage.getItem('coral:dashboardNote')) || 'show' }; } @@ -54,8 +61,11 @@ class CountdownTimer extends React.Component { } dismissNote = () => { + const {storage} = this.context; try { - window.localStorage.setItem('coral:dashboardNote', 'hide'); + if (storage) { + storage.setItem('coral:dashboardNote', 'hide'); + } } catch (e) { // when setItem fails in Safari Private mode @@ -64,7 +74,8 @@ class CountdownTimer extends React.Component { } render () { - const hideReloadNote = window.localStorage.getItem('coral:dashboardNote') === 'hide' || + const {storage} = this.context; + const hideReloadNote = (storage && storage.getItem('coral:dashboardNote') === 'hide') || this.state.dashboardNote === 'hide'; // for Safari Incognito return (

({ // AUTH TOKEN //============================================================================== -export const handleAuthToken = (token) => (dispatch) => { - Storage.setItem('exp', jwtDecode(token).exp); - Storage.setItem('token', token); +export const handleAuthToken = (token) => (dispatch, _, {storage}) => { + if (storage) { + storage.setItem('exp', jwtDecode(token).exp); + storage.setItem('token', token); + } dispatch({type: 'HANDLE_AUTH_TOKEN'}); }; @@ -272,9 +273,12 @@ export const fetchForgotPassword = (email) => (dispatch, getState, {rest}) => { // LOGOUT //============================================================================== -export const logout = () => async (dispatch, _, {rest, client, pym}) => { +export const logout = () => async (dispatch, _, {rest, client, pym, storage}) => { await rest('/auth', {method: 'DELETE'}); - Storage.removeItem('token'); + + if (storage) { + storage.removeItem('token'); + } // Reset the websocket. client.resetWebsocket(); @@ -296,12 +300,14 @@ const checkLoginSuccess = (user, isAdmin) => ({ isAdmin }); -export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { +export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => { dispatch(checkLoginRequest()); rest('/auth') .then((result) => { if (!result.user) { - Storage.removeItem('token'); + if (storage) { + storage.removeItem('token'); + } throw new Error('Not logged in'); } @@ -318,10 +324,10 @@ export const checkLogin = () => (dispatch, _, {rest, client, pym}) => { }) .catch((error) => { console.error(error); - if (error.status && error.status === 401) { + if (error.status && error.status === 401 && storage) { // Unauthorized. - Storage.removeItem('token'); + storage.removeItem('token'); } const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString(); dispatch(checkLoginFailure(errorMessage)); diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js index b68fc7a1a..b553cff45 100644 --- a/client/coral-framework/components/TalkProvider.js +++ b/client/coral-framework/components/TalkProvider.js @@ -11,6 +11,7 @@ class TalkProvider extends React.Component { rest: this.props.rest, graphqlRegistry: this.props.graphqlRegistry, notification: this.props.notification, + storage: this.props.storage, }; } @@ -31,6 +32,7 @@ TalkProvider.childContextTypes = { rest: PropTypes.func, graphqlRegistry: PropTypes.object, notification: PropTypes.object, + storage: PropTypes.object, }; export default TalkProvider; diff --git a/client/coral-framework/helpers/storage.js b/client/coral-framework/helpers/storage.js deleted file mode 100644 index ec83c3fb3..000000000 --- a/client/coral-framework/helpers/storage.js +++ /dev/null @@ -1,92 +0,0 @@ -let available, error; - -function storageAvailable(type) { - let storage = window[type], x = '__storage_test__'; - try { - storage.setItem(x, x); - storage.removeItem(x); - return true; - } catch (e) { - error = e; - return ( - e instanceof DOMException && - - // everything except Firefox - (e.code === 22 || - - // Firefox - - e.code === 1014 || - - // test name field too, because code might not be present - - // everything except Firefox - e.name === 'QuotaExceededError' || - - // Firefox - e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && - - // acknowledge QuotaExceededError only if there's something already stored - storage.length !== 0 - ); - } -} - -function lazyCheckStorage() { - if (typeof available === 'undefined') { - available = storageAvailable('localStorage'); - } -} - -export function getItem(item = '') { - lazyCheckStorage(); - - if (available) { - return localStorage.getItem(item); - } else { - console.error( - `Cannot get from localStorage. localStorage is not available. ${error}` - ); - } -} - -export function setItem(item = '', value) { - lazyCheckStorage(); - - if (available) { - return localStorage.setItem(item, value); - } else { - console.error( - `Cannot set localStorage. localStorage is not available. ${error}` - ); - } -} - -export function removeItem(item = '') { - lazyCheckStorage(); - - if (available) { - return localStorage.removeItem(item); - } else { - console.error( - `Cannot remove item from localStorage. localStorage is not available. ${error}` - ); - } -} - -export function clear() { - lazyCheckStorage(); - - if (available) { - return localStorage.clear(); - } else { - console.error( - `Cannot clear localStorage. localStorage is not available. ${error}` - ); - } -} - -// window.addEventListener('storage', function(e) { -// const msg = `${e.key} " was changed in page ${e.url} from ${e.oldValue} to ${e.newValue}`; -// console.log(msg); -// }); diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js index aa85e9d21..a1b8aec12 100644 --- a/client/coral-framework/services/bootstrap.js +++ b/client/coral-framework/services/bootstrap.js @@ -7,12 +7,12 @@ import {createRestClient} from './rest'; import thunk from 'redux-thunk'; import {loadTranslations} from './i18n'; import bowser from 'bowser'; -import * as Storage from '../helpers/storage'; import {BASE_PATH} from 'coral-framework/constants/url'; import {createPluginsService} from './plugins'; import {createNotificationService} from './notification'; import {createGraphQLRegistry} from './graphqlRegistry'; import globalFragments from 'coral-framework/graphql/fragments'; +import {createStorage} from 'coral-framework/services/storage'; /** * getAuthToken returns the active auth token or null @@ -20,7 +20,7 @@ import globalFragments from 'coral-framework/graphql/fragments'; * browsers that don't allow us to use cross domain iframe local storage. * @return {string|null} */ -const getAuthToken = (store) => { +const getAuthToken = (store, storage) => { let state = store.getState(); if (state.config.auth_token) { @@ -28,10 +28,10 @@ const getAuthToken = (store) => { // if an auth_token exists in config, use it. return state.config.auth_token; - } else if (!bowser.safari && !bowser.ios) { + } else if (!bowser.safari && !bowser.ios && storage) { // Use local storage auth tokens where there's a stable api. - return Storage.getItem('token'); + return storage.getItem('token'); } return null; @@ -40,6 +40,7 @@ const getAuthToken = (store) => { export function createContext({reducers = {}, pluginsConfig = [], graphqlExtension = {}, notification}) { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const eventEmitter = new EventEmitter({wildcard: true}); + const storage = createStorage(); let store = null; const token = () => { @@ -48,7 +49,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi // NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT // TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT. - return getAuthToken(store); + return getAuthToken(store, storage); }; const rest = createRestClient({ uri: `${BASE_PATH}api/v1`, @@ -74,6 +75,7 @@ export function createContext({reducers = {}, pluginsConfig = [], graphqlExtensi rest, graphqlRegistry, notification, + storage, }; // Load framework fragments. diff --git a/client/coral-framework/services/storage.js b/client/coral-framework/services/storage.js new file mode 100644 index 000000000..1454d2ac5 --- /dev/null +++ b/client/coral-framework/services/storage.js @@ -0,0 +1,39 @@ + +function getStorage(type) { + let storage = window[type], x = '__storage_test__'; + try { + storage.setItem(x, x); + storage.removeItem(x); + } catch (e) { + const ignore = ( + e instanceof DOMException && + + // everything except Firefox + (e.code === 22 || + + // Firefox + + e.code === 1014 || + + // test name field too, because code might not be present + + // everything except Firefox + e.name === 'QuotaExceededError' || + + // Firefox + e.name === 'NS_ERROR_DOM_QUOTA_REACHED') && + + // acknowledge QuotaExceededError only if there's something already stored + storage.length !== 0 + ); + if (!ignore) { + console.warning(e); // eslint-disable-line + return null; + } + } + return storage; +} + +export function createStorage() { + return getStorage('localStorage'); +}