diff --git a/client/components/forceProtocol.jsx b/client/components/forceProtocol.jsx index 5a583bc4..dda5451c 100644 --- a/client/components/forceProtocol.jsx +++ b/client/components/forceProtocol.jsx @@ -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, +}; diff --git a/client/constants.js b/client/constants.js index 3cc8f104..e3060237 100644 --- a/client/constants.js +++ b/client/constants.js @@ -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'; diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index ae6170a8..c5abc528 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -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(); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 31f0af47..b8b501f7 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -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) { diff --git a/client/modules/IDE/reducers/project.js b/client/modules/IDE/reducers/project.js index d633dd48..1b8faa52 100644 --- a/client/modules/IDE/reducers/project.js +++ b/client/modules/IDE/reducers/project.js @@ -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(), }; }; diff --git a/client/persistState.js b/client/persistState.js new file mode 100644 index 00000000..457dba01 --- /dev/null +++ b/client/persistState.js @@ -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); +}; diff --git a/client/routes.jsx b/client/routes.jsx index 33b3cf38..b4bdf6a8 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -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, diff --git a/client/store.js b/client/store.js index 023197c9..1fe11cc3 100644 --- a/client/store.js +++ b/client/store.js @@ -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) ); diff --git a/client/utils/isSecurePage.js b/client/utils/isSecurePage.js new file mode 100644 index 00000000..2efb908a --- /dev/null +++ b/client/utils/isSecurePage.js @@ -0,0 +1,6 @@ + +const isSecurePage = () => ( + window.location.protocol === 'https:' +); + +export default isSecurePage; diff --git a/package.json b/package.json index 7e0e55f2..1653cac6 100644 --- a/package.json +++ b/package.json @@ -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",