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 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,
|
||||||
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
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 { 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,
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
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",
|
"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",
|
||||||
|
|
Loading…
Reference in a new issue