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 { format, parse } from 'url';
/**
* 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
* 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 {
static propTypes = {}
componentDidMount() {
this.redirectToProtocol(targetProtocol);
this.redirectToProtocol(targetProtocol, { appendSource: true });
}
componentWillUnmount() {
if (sourceProtocol != null) {
this.redirectToProtocol(sourceProtocol);
this.redirectToProtocol(sourceProtocol, { appendSource: false });
}
}
redirectToProtocol(protocol) {
const currentProtocol = window.location.protocol;
redirectToProtocol(protocol, { appendSource }) {
const currentProtocol = parse(window.location.href).protocol;
if (protocol !== currentProtocol) {
if (disable === true) {
console.info(`forceProtocol: would have redirected from "${currentProtocol}" to "${protocol}"`);
} 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 {
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 SHOW_ERROR_MODAL = 'SHOW_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,
resetJustOpenedProject,
showErrorModal } from './ide';
import { clearState, saveState } from '../../../persistState';
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) {
return (dispatch, getState) => {
const state = getState();

View file

@ -40,6 +40,10 @@ class IDEView extends React.Component {
}
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();
if (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
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
// don't warn
this.props.persistState();
window.onbeforeunload = null;
} else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
// don't warn
this.props.persistState();
window.onbeforeunload = null;
} else if (this.props.ide.unsavedChanges) {
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
return false;
@ -590,7 +598,9 @@ IDEView.propTypes = {
})).isRequired,
clearConsole: 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) {

View file

@ -1,12 +1,13 @@
import generate from 'project-name-generator';
import * as ActionTypes from '../../../constants';
import isSecurePage from '../../../utils/isSecurePage';
const initialState = () => {
const generatedString = generate({ words: 2 }).spaced;
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
return {
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 React from 'react';
import forceProtocol from './components/forceProtocol';
import forceProtocol, { protocols, findSourceProtocol } from './components/forceProtocol';
import App from './modules/App/App';
import IDEView from './modules/IDE/pages/IDEView';
import FullView from './modules/IDE/pages/FullView';
@ -17,13 +17,11 @@ const checkAuth = (store) => {
};
const routes = (store) => {
const sourceProtocol = store.getState().project.serveSecure === true ?
'https:' :
'http:';
const sourceProtocol = findSourceProtocol(store.getState());
// If the flag is false, we stay on HTTP
const forceToHttps = forceProtocol({
targetProtocol: 'https:',
targetProtocol: protocols.https,
sourceProtocol,
// prints debugging but does not reload page
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 DevTools from './modules/App/components/DevTools';
import rootReducer from './reducers';
import { clearState, loadState } from './persistState';
export default function configureStore(initialState) {
const enhancers = [
@ -13,9 +14,12 @@ export default function configureStore(initialState) {
enhancers.push(window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument());
}
const savedState = loadState();
clearState();
const store = createStore(
rootReducer,
initialState,
savedState != null ? savedState : initialState,
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",
"shortid": "^2.2.6",
"srcdoc-polyfill": "^0.2.0",
"url": "^0.11.0",
"webpack": "^1.14.0",
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.10.0",