Persists Redux store to/from sessionStorage (#334)

* Persists Redux store when reloading app for login

* Disable confirmation box when leaving page for login

* Removes extra console.warn

* Sets serveSecure: true for new projects if served over HTTPS

* Clears persisted state on IDEView load

Because when a sketch is created on HTTPS and then the user logs in
the page won't be reloaded

* Appends ?source=<protocol> to URL to track return protocol
This commit is contained in:
Andrew Nicolaou 2017-04-20 20:05:15 +02:00 committed by Cassie Tarakajian
parent a4a1a36f02
commit a267837fb7
10 changed files with 110 additions and 14 deletions

View file

@ -1,4 +1,5 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { format, parse } from 'url';
/** /**
* A Higher Order Component that forces the protocol to change on mount * A Higher Order Component that forces the protocol to change on mount
@ -8,28 +9,33 @@ import React, { PropTypes } from 'react';
* disable: if true, the redirection will not happen but what should * disable: if true, the redirection will not happen but what should
* have happened will be logged to the console * have happened will be logged to the console
*/ */
const forceProtocol = ({ targetProtocol = 'https:', sourceProtocol, disable = false }) => WrappedComponent => ( const forceProtocol = ({ targetProtocol = 'https', sourceProtocol, disable = false }) => WrappedComponent => (
class ForceProtocol extends React.Component { class ForceProtocol extends React.Component {
static propTypes = {} static propTypes = {}
componentDidMount() { componentDidMount() {
this.redirectToProtocol(targetProtocol); this.redirectToProtocol(targetProtocol, { appendSource: true });
} }
componentWillUnmount() { componentWillUnmount() {
if (sourceProtocol != null) { if (sourceProtocol != null) {
this.redirectToProtocol(sourceProtocol); this.redirectToProtocol(sourceProtocol, { appendSource: false });
} }
} }
redirectToProtocol(protocol) { redirectToProtocol(protocol, { appendSource }) {
const currentProtocol = window.location.protocol; const currentProtocol = parse(window.location.href).protocol;
if (protocol !== currentProtocol) { if (protocol !== currentProtocol) {
if (disable === true) { if (disable === true) {
console.info(`forceProtocol: would have redirected from "${currentProtocol}" to "${protocol}"`); console.info(`forceProtocol: would have redirected from "${currentProtocol}" to "${protocol}"`);
} else { } else {
window.location = window.location.href.replace(currentProtocol, protocol); const url = parse(window.location.href, true /* parse query string */);
url.protocol = protocol;
if (appendSource === true) {
url.query.source = currentProtocol;
}
window.location = format(url);
} }
} }
} }
@ -40,5 +46,25 @@ const forceProtocol = ({ targetProtocol = 'https:', sourceProtocol, disable = fa
} }
); );
const protocols = {
http: 'http:',
https: 'https:',
};
const findSourceProtocol = (state, location) => {
if (/source=https/.test(window.location.search)) {
return protocols.https;
} else if (/source=http/.test(window.location.search)) {
return protocols.http;
} else if (state.project.serveSecure === true) {
return protocols.https;
}
return protocols.http;
};
export default forceProtocol; export default forceProtocol;
export {
findSourceProtocol,
protocols,
};

View file

@ -110,3 +110,6 @@ export const RESET_PROJECT_SAVED_TIME = 'RESET_PROJECT_SAVED_TIME';
export const SET_PREVIOUS_PATH = 'SET_PREVIOUS_PATH'; export const SET_PREVIOUS_PATH = 'SET_PREVIOUS_PATH';
export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL'; export const SHOW_ERROR_MODAL = 'SHOW_ERROR_MODAL';
export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL'; export const HIDE_ERROR_MODAL = 'HIDE_ERROR_MODAL';
export const PERSIST_STATE = 'PERSIST_STATE';
export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE';

View file

@ -8,6 +8,7 @@ import { setUnsavedChanges,
justOpenedProject, justOpenedProject,
resetJustOpenedProject, resetJustOpenedProject,
showErrorModal } from './ide'; showErrorModal } from './ide';
import { clearState, saveState } from '../../../persistState';
const ROOT_URL = process.env.API_URL; const ROOT_URL = process.env.API_URL;
@ -42,6 +43,25 @@ export function getProject(id) {
}; };
} }
export function persistState() {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.PERSIST_STATE,
});
const state = getState();
saveState(state);
};
}
export function clearPersistedState() {
return (dispatch) => {
dispatch({
type: ActionTypes.CLEAR_PERSISTED_STATE,
});
clearState();
};
}
export function saveProject(autosave = false) { export function saveProject(autosave = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();

View file

@ -40,6 +40,10 @@ class IDEView extends React.Component {
} }
componentDidMount() { componentDidMount() {
// If page doesn't reload after Sign In then we need
// to force cleared state to be cleared
this.props.clearPersistedState();
this.props.stopSketch(); this.props.stopSketch();
if (this.props.params.project_id) { if (this.props.params.project_id) {
const id = this.props.params.project_id; const id = this.props.params.project_id;
@ -170,8 +174,12 @@ class IDEView extends React.Component {
warnIfUnsavedChanges(route) { // eslint-disable-line warnIfUnsavedChanges(route) { // eslint-disable-line
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) { if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
// don't warn // don't warn
this.props.persistState();
window.onbeforeunload = null;
} else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) { } else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
// don't warn // don't warn
this.props.persistState();
window.onbeforeunload = null;
} else if (this.props.ide.unsavedChanges) { } else if (this.props.ide.unsavedChanges) {
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) { if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
return false; return false;
@ -590,7 +598,9 @@ IDEView.propTypes = {
})).isRequired, })).isRequired,
clearConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired,
hideErrorModal: PropTypes.func.isRequired hideErrorModal: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired,
persistState: PropTypes.func.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {

View file

@ -1,12 +1,13 @@
import generate from 'project-name-generator'; import generate from 'project-name-generator';
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
import isSecurePage from '../../../utils/isSecurePage';
const initialState = () => { const initialState = () => {
const generatedString = generate({ words: 2 }).spaced; const generatedString = generate({ words: 2 }).spaced;
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1); const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
return { return {
name: generatedName, name: generatedName,
serveSecure: false, serveSecure: isSecurePage(),
}; };
}; };

27
client/persistState.js Normal file
View file

@ -0,0 +1,27 @@
/*
Saves and loads a snapshot of the Redux store
state to session storage
*/
const key = 'p5js-editor';
const storage = sessionStorage;
export const saveState = (state) => {
try {
storage.setItem(key, JSON.stringify(state));
} catch (error) {
console.warn('Unable to persist state to storage:', error);
}
};
export const loadState = () => {
try {
return JSON.parse(storage.getItem(key));
} catch (error) {
console.warn('Failed to retrieve initialize state from storage:', error);
return null;
}
};
export const clearState = () => {
storage.removeItem(key);
};

View file

@ -1,6 +1,6 @@
import { Route, IndexRoute } from 'react-router'; import { Route, IndexRoute } from 'react-router';
import React from 'react'; import React from 'react';
import forceProtocol from './components/forceProtocol'; import forceProtocol, { protocols, findSourceProtocol } from './components/forceProtocol';
import App from './modules/App/App'; import App from './modules/App/App';
import IDEView from './modules/IDE/pages/IDEView'; import IDEView from './modules/IDE/pages/IDEView';
import FullView from './modules/IDE/pages/FullView'; import FullView from './modules/IDE/pages/FullView';
@ -17,13 +17,11 @@ const checkAuth = (store) => {
}; };
const routes = (store) => { const routes = (store) => {
const sourceProtocol = store.getState().project.serveSecure === true ? const sourceProtocol = findSourceProtocol(store.getState());
'https:' :
'http:';
// If the flag is false, we stay on HTTP // If the flag is false, we stay on HTTP
const forceToHttps = forceProtocol({ const forceToHttps = forceProtocol({
targetProtocol: 'https:', targetProtocol: protocols.https,
sourceProtocol, sourceProtocol,
// prints debugging but does not reload page // prints debugging but does not reload page
disable: process.env.FORCE_TO_HTTPS === false, disable: process.env.FORCE_TO_HTTPS === false,

View file

@ -2,6 +2,7 @@ import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import DevTools from './modules/App/components/DevTools'; import DevTools from './modules/App/components/DevTools';
import rootReducer from './reducers'; import rootReducer from './reducers';
import { clearState, loadState } from './persistState';
export default function configureStore(initialState) { export default function configureStore(initialState) {
const enhancers = [ const enhancers = [
@ -13,9 +14,12 @@ export default function configureStore(initialState) {
enhancers.push(window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument()); enhancers.push(window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument());
} }
const savedState = loadState();
clearState();
const store = createStore( const store = createStore(
rootReducer, rootReducer,
initialState, savedState != null ? savedState : initialState,
compose(...enhancers) compose(...enhancers)
); );

View file

@ -0,0 +1,6 @@
const isSecurePage = () => (
window.location.protocol === 'https:'
);
export default isSecurePage;

View file

@ -112,6 +112,7 @@
"s3-policy": "^0.2.0", "s3-policy": "^0.2.0",
"shortid": "^2.2.6", "shortid": "^2.2.6",
"srcdoc-polyfill": "^0.2.0", "srcdoc-polyfill": "^0.2.0",
"url": "^0.11.0",
"webpack": "^1.14.0", "webpack": "^1.14.0",
"webpack-dev-middleware": "^1.6.1", "webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.10.0", "webpack-hot-middleware": "^2.10.0",