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:
parent
a4a1a36f02
commit
a267837fb7
10 changed files with 110 additions and 14 deletions
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
27
client/persistState.js
Normal 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);
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
6
client/utils/isSecurePage.js
Normal file
6
client/utils/isSecurePage.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
const isSecurePage = () => (
|
||||
window.location.protocol === 'https:'
|
||||
);
|
||||
|
||||
export default isSecurePage;
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue