From 31a2389d04b7fb2416bf71ae70f8b4aa72818199 Mon Sep 17 00:00:00 2001 From: Benedict Aas Date: Thu, 8 Jun 2017 16:42:55 +0100 Subject: [PATCH] fix(GUI): reset webview after navigating away (#1422) We reload and reset the webview to its original URL when the user navigates away from the success screen. Changelog-Entry: Reset webview after navigating away from the success screen. --- lib/gui/app.js | 37 ++- lib/gui/components/safe-webview.js | 247 ++++++++++++++++++ .../components/safe-webview/safe-webview.js | 173 ------------ lib/gui/index.html | 7 +- 4 files changed, 284 insertions(+), 180 deletions(-) create mode 100644 lib/gui/components/safe-webview.js delete mode 100644 lib/gui/components/safe-webview/safe-webview.js diff --git a/lib/gui/app.js b/lib/gui/app.js index 674fc9e9..12c797af 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -59,7 +59,7 @@ const app = angular.module('Etcher', [ // Components require('./components/svg-icon/svg-icon'), require('./components/warning-modal/warning-modal'), - require('./components/safe-webview/safe-webview'), + require('./components/safe-webview'), // Pages require('./pages/main/main'), @@ -300,11 +300,40 @@ app.controller('HeaderController', function(SelectionStateModel, OSOpenExternalS }); -app.controller('StateController', function($state) { +app.controller('StateController', function($rootScope, $scope) { + const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => { + this.previousName = fromState.name; + this.currentName = toState.name; + }); + + $scope.$on('$destroy', unregisterStateChange); /** - * @param {string} state - state page + * @summary Get the previous state name + * @function + * @public + * + * @returns {String} previous state name + * + * @example + * if (StateController.previousName === 'main') { + * console.log('We left the main screen!'); + * } */ - this.is = $state.is; + this.previousName = null; + + /** + * @summary Get the current state name + * @function + * @public + * + * @returns {String} current state name + * + * @example + * if (StateController.currentName === 'main') { + * console.log('We are on the main screen!'); + * } + */ + this.currentName = null; }); diff --git a/lib/gui/components/safe-webview.js b/lib/gui/components/safe-webview.js new file mode 100644 index 00000000..ad5f9d41 --- /dev/null +++ b/lib/gui/components/safe-webview.js @@ -0,0 +1,247 @@ +/* + * Copyright 2017 resin.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const _ = require('lodash'); +const electron = require('electron'); +const angular = require('angular'); +const react = require('react'); +const propTypes = require('prop-types'); +const react2angular = require('react2angular').react2angular; +const analytics = require('../modules/analytics'); +const packageJSON = require('../../../package.json'); +const robot = require('../../shared/robot'); + +const MODULE_NAME = 'Etcher.Components.SafeWebview'; +const angularSafeWebview = angular.module(MODULE_NAME, []); + +/** + * @summary GET parameter sent to the initial webview source URL + * @constant + * @private + * @type {String} + */ +const VERSION_PARAM = 'etcher-version'; + +/** + * @summary Electron session identifier + * @constant + * @private + * @type {String} + */ +const ELECTRON_SESSION = 'persist:success-banner'; + +/** + * @summary Webviews that hide/show depending on the HTTP status returned + * @type {Object} + * @public + * + * @example + * + */ +class SafeWebview extends react.PureComponent { + + /** + * @param {Object} props - React element properties + */ + constructor(props) { + super(props); + + this.state = { + shouldShow: true + }; + + const url = new URL(props.src); + + // We set the version GET parameter here. + url.searchParams.set(VERSION_PARAM, packageJSON.version); + + this.entryHref = url.href; + + // Events steal 'this' + this.didFailLoad = _.bind(this.didFailLoad, this); + this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this); + + this.eventTuples = [ + [ 'did-fail-load', this.didFailLoad ], + [ 'did-get-response-details', this.didGetResponseDetails ], + [ 'new-window', this.constructor.newWindow ], + [ 'console-message', this.constructor.consoleMessage ] + ]; + + // Make a persistent electron session for the webview + electron.remote.session.fromPartition(ELECTRON_SESSION, { + + // Disable the cache for the session such that new content shows up when refreshing + cache: false + }); + } + + /** + * @returns {react.Element} + */ + render() { + return react.createElement('webview', { + ref: 'webview', + style: { + flex: this.state.shouldShow ? null : '0 1', + width: this.state.shouldShow ? null : '0', + height: this.state.shouldShow ? null : '0' + } + }, []); + } + + /** + * @summary Add the Webview events + */ + componentDidMount() { + + // Events React is unaware of have to be handled manually + _.map(this.eventTuples, (tuple) => { + this.refs.webview.addEventListener(...tuple); + }); + + // Use the 'success-banner' session + this.refs.webview.partition = ELECTRON_SESSION; + + // It's important that this comes after the partition setting, otherwise it will + // use another session and we can't change it without destroying the element again + this.refs.webview.src = this.entryHref; + } + + /** + * @summary Remove the Webview events + */ + componentWillUnmount() { + + // Events that React is unaware of have to be handled manually + _.map(this.eventTuples, (tuple) => { + this.refs.webview.removeEventListener(...tuple); + }); + } + + /** + * @summary Refresh the webview if we are navigating away from the success page + * @param {Object} nextProps - upcoming properties + */ + componentWillReceiveProps(nextProps) { + if (nextProps.refreshNow && !this.props.refreshNow) { + + // Reload the page if it hasn't changed, otherwise reset the source URL, + // because reload interferes with 'src' setting, resetting the 'src' attribute + // to what it was was just prior. + if (this.refs.webview.src === this.entryHref) { + this.refs.webview.reload(); + + } else { + this.refs.webview.src = this.entryHref; + } + + this.setState({ + shouldShow: true + }); + } + } + + /** + * @summary Set the element state to hidden + */ + didFailLoad() { + this.setState({ + shouldShow: false + }); + } + + /** + * @summary Set the element state depending on the HTTP response code + * @param {Event} event - Event object + */ + didGetResponseDetails(event) { + const HTTP_OK = 200; + const HTTP_ERR = 400; + + this.setState({ + shouldShow: event.httpResponseCode >= HTTP_OK && event.httpResponseCode < HTTP_ERR + }); + } + + /** + * @summary Open link in browser if it's opened as a 'foreground-tab' + * @param {Event} event - event object + */ + static newWindow(event) { + const url = new URL(event.url); + + if (_.every([ + url.protocol === 'http:' || url.protocol === 'https:', + event.disposition === 'foreground-tab' + ])) { + electron.shell.openExternal(url.href); + } + } + + /** + * @summary Forward specially-formatted console messages from the webview + * @param {Event} event - event object + * + * @example + * + * // In the webview + * console.log(JSON.stringify({ + * command: 'error', + * data: 'Good night!' + * })); + * + * console.log(JSON.stringify({ + * command: 'log', + * data: 'Hello, Mars!' + * })); + */ + static consoleMessage(event) { + if (!robot.isMessage(event.message)) { + return; + } + + const message = robot.parseMessage(event.message); + + if (robot.getCommand(message) === robot.COMMAND.LOG) { + analytics.logEvent(robot.getData(message)); + + } else if (robot.getCommand(message) === robot.COMMAND.ERROR) { + analytics.logException(robot.getData(message)); + } + } + +} + +SafeWebview.propTypes = { + + /** + * @summary The website source URL + */ + src: propTypes.string.isRequired, + + /** + * @summary Refresh the webview + */ + refreshNow: propTypes.bool + +}; + +angularSafeWebview.component('safeWebview', react2angular(SafeWebview)); + +module.exports = MODULE_NAME; diff --git a/lib/gui/components/safe-webview/safe-webview.js b/lib/gui/components/safe-webview/safe-webview.js deleted file mode 100644 index bc31812b..00000000 --- a/lib/gui/components/safe-webview/safe-webview.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2017 resin.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const _ = require('lodash'); -const electron = require('electron'); -const angular = require('angular'); -const react = require('react'); -const propTypes = require('prop-types'); -const react2angular = require('react2angular').react2angular; -const analytics = require('../../modules/analytics'); -const packageJSON = require('../../../../package.json'); - -const MODULE_NAME = 'Etcher.Components.SafeWebview'; -const angularSafeWebview = angular.module(MODULE_NAME, []); - -/** - * @summary Webviews that time out - * @type {Object} - * @public - */ -class SafeWebview extends react.PureComponent { - - /** - * @param {Object} props - React element properties - */ - constructor(props) { - super(props); - - this.state = { - shouldLoad: true - }; - - // Events steal 'this' - this.didFailLoad = _.bind(this.didFailLoad, this); - this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this); - } - - /** - * @returns {react.Element} - */ - render() { - if (this.state.shouldLoad) { - const url = new URL(this.props.src); - - // We set the 'etcher-version' GET parameter here. - url.searchParams.set('etcher-version', packageJSON.version); - - return react.createElement('webview', { - ref: 'webview', - src: url.href - }, []); - } - - // We have to return null explicitly, undefined is an error in React. - return null; - } - - /** - * @summary Add the Webview events if there is an element - */ - componentDidMount() { - - // There is no element to add events to if 'shouldLoad' is false. - if (this.state.shouldLoad) { - - // Events React is unaware of have to be handled manually - this.refs.webview.addEventListener('did-fail-load', this.didFailLoad); - this.refs.webview.addEventListener('did-get-response-details', this.didGetResponseDetails); - this.refs.webview.addEventListener('new-window', this.constructor.newWindow); - this.refs.webview.addEventListener('console-message', this.constructor.consoleMessage); - - } - } - - /** - * @summary Remove the Webview events if there is an element - */ - componentWillUnmount() { - - // There is no element to remove events from if 'shouldLoad' is false. - if (this.state.shouldLoad) { - - // Events React is unaware of have to be handled manually - this.refs.webview.removeEventListener('did-fail-load', this.didFailLoad); - this.refs.webview.removeEventListener('did-get-response-details', this.didGetResponseDetails); - this.refs.webview.removeEventListener('new-window', this.constructor.newWindow); - this.refs.webview.removeEventListener('console-message', this.constructor.consoleMessage); - - } - - } - - /** - * @summary Set the element state to hidden - */ - didFailLoad() { - this.setState({ - shouldLoad: false - }); - } - - /** - * @summary Set the element state depending on the HTTP response code - * @param {Event} event - Event object - */ - didGetResponseDetails(event) { - const HTTP_OK = 200; - const HTTP_ERR = 400; - - if (event.httpResponseCode < HTTP_OK || event.httpResponseCode >= HTTP_ERR) { - this.setState({ - shouldLoad: false - }); - } - } - - /** - * @param {Event} event - event object - */ - static newWindow(event) { - const url = new URL(event.url); - - if (_.every([ - url.protocol === 'http:' || url.protocol === 'https:', - event.disposition === 'foreground-tab' - ])) { - electron.shell.openExternal(url.href); - } - } - - /** - * @param {Event} event - event object - */ - static consoleMessage(event) { - const ERROR_LEVEL = 2; - - if (event.level < ERROR_LEVEL) { - analytics.logEvent(event.message); - - } else if (event.level === ERROR_LEVEL) { - analytics.logException(event.message); - } - } - -} - -SafeWebview.propTypes = { - - /** - * @summary The website source URL - */ - src: propTypes.string.isRequired - -}; - -angularSafeWebview.component('safeWebview', react2angular(SafeWebview)); - -module.exports = MODULE_NAME; diff --git a/lib/gui/index.html b/lib/gui/index.html index 53aaec80..f7d1e9c6 100644 --- a/lib/gui/index.html +++ b/lib/gui/index.html @@ -31,7 +31,7 @@