diff --git a/lib/gui/app/components/featured-project/featured-project.tsx b/lib/gui/app/components/featured-project/featured-project.tsx
index f24c7531..49426a8d 100644
--- a/lib/gui/app/components/featured-project/featured-project.tsx
+++ b/lib/gui/app/components/featured-project/featured-project.tsx
@@ -18,7 +18,7 @@ import * as React from 'react';
import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
-import * as SafeWebview from '../safe-webview/safe-webview.jsx';
+import { SafeWebview } from '../safe-webview/safe-webview';
interface FeaturedProjectProps {
onWebviewShow: (isWebviewShowing: boolean) => void;
diff --git a/lib/gui/app/components/safe-webview/safe-webview.jsx b/lib/gui/app/components/safe-webview/safe-webview.jsx
deleted file mode 100644
index 2b06fd2e..00000000
--- a/lib/gui/app/components/safe-webview/safe-webview.jsx
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright 2017 balena.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'
-
-/* eslint-disable jsdoc/require-example */
-
-const _ = require('lodash')
-const electron = require('electron')
-const react = require('react')
-const propTypes = require('prop-types')
-const analytics = require('../../modules/analytics')
-const { store } = require('../../models/store')
-const settings = require('../../models/settings')
-const packageJSON = require('../../../../../package.json')
-
-/**
- * @summary Electron session identifier
- * @constant
- * @private
- * @type {String}
- */
-const ELECTRON_SESSION = 'persist:success-banner'
-
-/**
- * @summary Etcher version search-parameter key
- * @constant
- * @private
- * @type {String}
- */
-const ETCHER_VERSION_PARAM = 'etcher-version'
-
-/**
- * @summary API version search-parameter key
- * @constant
- * @private
- * @type {String}
- */
-const API_VERSION_PARAM = 'api-version'
-
-/**
- * @summary Opt-out analytics search-parameter key
- * @constant
- * @private
- * @type {String}
- */
-const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics'
-
-/**
- * @summary Webview API version
- * @constant
- * @private
- * @type {String}
- *
- * @description
- * Changing this number represents a departure from an older API and as such
- * should only be changed when truly necessary as it introduces breaking changes.
- * This version number is exposed to the banner such that it can determine what
- * features are safe to utilize.
- *
- * See `git blame -L n` where n is the line below for the history of version changes.
- */
-const API_VERSION = 2
-
-/**
- * @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 window.URL(props.src)
-
- // We set the version GET parameters here.
- url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version)
- url.searchParams.set(API_VERSION_PARAM, API_VERSION)
- url.searchParams.set(OPT_OUT_ANALYTICS_PARAM, !settings.get('errorReporting'))
-
- this.entryHref = url.href
-
- // Events steal 'this'
- this.didFailLoad = _.bind(this.didFailLoad, this)
- this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this)
-
- const logWebViewMessage = (event) => {
- console.log('Message from SafeWebview:', event.message)
- }
-
- this.eventTuples = [
- [ 'did-fail-load', this.didFailLoad ],
- [ 'new-window', this.constructor.newWindow ],
- [ 'console-message', logWebViewMessage ]
- ]
-
- // Make a persistent electron session for the webview
- this.session = 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',
- partition: ELECTRON_SESSION,
- 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)
- })
-
- this.session.webRequest.onCompleted(this.didGetResponseDetails)
-
- // 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)
- })
- this.session.webRequest.onCompleted(null)
- }
-
- /**
- * @summary Set the element state to hidden
- */
- didFailLoad () {
- this.setState({
- shouldShow: false
- })
- if (this.props.onWebviewShow) {
- this.props.onWebviewShow(false)
- }
- }
-
- /**
- * @summary Set the element state depending on the HTTP response code
- * @param {Event} event - Event object
- */
- didGetResponseDetails (event) {
- // This seems to pick up all requests related to the webview,
- // only care about this event if it's a request for the main frame
- if (event.resourceType === 'mainFrame') {
- const HTTP_OK = 200
-
- analytics.logEvent('SafeWebview loaded', {
- event,
- applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
- flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
- })
-
- this.setState({
- shouldShow: event.statusCode === HTTP_OK
- })
- if (this.props.onWebviewShow) {
- this.props.onWebviewShow(event.statusCode === HTTP_OK)
- }
- }
- }
-
- /**
- * @summary Open link in browser if it's opened as a 'foreground-tab'
- * @param {Event} event - event object
- */
- static newWindow (event) {
- const url = new window.URL(event.url)
-
- if (_.every([
- url.protocol === 'http:' || url.protocol === 'https:',
- event.disposition === 'foreground-tab',
-
- // Don't open links if they're disabled by the env var
- !settings.get('disableExternalLinks')
- ])) {
- electron.shell.openExternal(url.href)
- }
- }
-}
-
-SafeWebview.propTypes = {
-
- /**
- * @summary The website source URL
- */
- src: propTypes.string.isRequired,
-
- /**
- * @summary Refresh the webview
- */
- refreshNow: propTypes.bool,
-
- /**
- * @summary Webview lifecycle event
- */
- onWebviewShow: propTypes.func
-
-}
-
-module.exports = SafeWebview
diff --git a/lib/gui/app/components/safe-webview/safe-webview.tsx b/lib/gui/app/components/safe-webview/safe-webview.tsx
new file mode 100644
index 00000000..96b410ce
--- /dev/null
+++ b/lib/gui/app/components/safe-webview/safe-webview.tsx
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2017 balena.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.
+ */
+
+import * as electron from 'electron';
+import * as _ from 'lodash';
+import * as React from 'react';
+
+import * as packageJSON from '../../../../../package.json';
+import * as settings from '../../models/settings';
+import { store } from '../../models/store';
+import * as analytics from '../../modules/analytics';
+
+/**
+ * @summary Electron session identifier
+ */
+const ELECTRON_SESSION = 'persist:success-banner';
+
+/**
+ * @summary Etcher version search-parameter key
+ */
+const ETCHER_VERSION_PARAM = 'etcher-version';
+
+/**
+ * @summary API version search-parameter key
+ */
+const API_VERSION_PARAM = 'api-version';
+
+/**
+ * @summary Opt-out analytics search-parameter key
+ */
+const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics';
+
+/**
+ * @summary Webview API version
+ *
+ * @description
+ * Changing this number represents a departure from an older API and as such
+ * should only be changed when truly necessary as it introduces breaking changes.
+ * This version number is exposed to the banner such that it can determine what
+ * features are safe to utilize.
+ *
+ * See `git blame -L n` where n is the line below for the history of version changes.
+ */
+const API_VERSION = '2';
+
+interface SafeWebviewProps {
+ // The website source URL
+ src: string;
+ // @summary Refresh the webview
+ refreshNow?: boolean;
+ // Webview lifecycle event
+ onWebviewShow?: (isWebviewShowing: boolean) => void;
+}
+
+interface SafeWebviewState {
+ shouldShow: boolean;
+}
+
+/**
+ * @summary Webviews that hide/show depending on the HTTP status returned
+ */
+export class SafeWebview extends React.PureComponent<
+ SafeWebviewProps,
+ SafeWebviewState
+> {
+ private entryHref: string;
+ private session: electron.Session;
+ private webviewRef: React.RefObject;
+
+ constructor(props: SafeWebviewProps) {
+ super(props);
+ this.webviewRef = React.createRef();
+ this.state = {
+ shouldShow: true,
+ };
+ const url = new window.URL(this.props.src);
+ // We set the version GET parameters here.
+ url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version);
+ url.searchParams.set(API_VERSION_PARAM, API_VERSION);
+ url.searchParams.set(
+ OPT_OUT_ANALYTICS_PARAM,
+ (!settings.get('errorReporting')).toString(),
+ );
+ this.entryHref = url.href;
+ // Events steal 'this'
+ this.didFailLoad = _.bind(this.didFailLoad, this);
+ this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
+ // Make a persistent electron session for the webview
+ this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
+ // Disable the cache for the session such that new content shows up when refreshing
+ cache: false,
+ });
+ }
+
+ private static logWebViewMessage(event: electron.ConsoleMessageEvent) {
+ console.log('Message from SafeWebview:', event.message);
+ }
+
+ public render() {
+ return (
+
+ );
+ }
+
+ // Add the Webview events
+ public componentDidMount() {
+ // Events React is unaware of have to be handled manually
+ if (this.webviewRef.current !== null) {
+ this.webviewRef.current.addEventListener(
+ 'did-fail-load',
+ this.didFailLoad,
+ );
+ this.webviewRef.current.addEventListener(
+ 'new-window',
+ SafeWebview.newWindow,
+ );
+ this.webviewRef.current.addEventListener(
+ 'console-message',
+ SafeWebview.logWebViewMessage,
+ );
+ this.session.webRequest.onCompleted(this.didGetResponseDetails);
+ // 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.webviewRef.current.src = this.entryHref;
+ }
+ }
+
+ // Remove the Webview events
+ public componentWillUnmount() {
+ // Events that React is unaware of have to be handled manually
+ if (this.webviewRef.current !== null) {
+ this.webviewRef.current.removeEventListener(
+ 'did-fail-load',
+ this.didFailLoad,
+ );
+ this.webviewRef.current.removeEventListener(
+ 'new-window',
+ SafeWebview.newWindow,
+ );
+ this.webviewRef.current.removeEventListener(
+ 'console-message',
+ SafeWebview.logWebViewMessage,
+ );
+ }
+ this.session.webRequest.onCompleted(null);
+ }
+
+ // Set the element state to hidden
+ public didFailLoad() {
+ this.setState({
+ shouldShow: false,
+ });
+ if (this.props.onWebviewShow) {
+ this.props.onWebviewShow(false);
+ }
+ }
+
+ // Set the element state depending on the HTTP response code
+ public didGetResponseDetails(event: electron.OnCompletedDetails) {
+ // This seems to pick up all requests related to the webview,
+ // only care about this event if it's a request for the main frame
+ if (event.resourceType === 'mainFrame') {
+ const HTTP_OK = 200;
+ analytics.logEvent('SafeWebview loaded', {
+ event,
+ applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
+ flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
+ });
+ this.setState({
+ shouldShow: event.statusCode === HTTP_OK,
+ });
+ if (this.props.onWebviewShow) {
+ this.props.onWebviewShow(event.statusCode === HTTP_OK);
+ }
+ }
+ }
+
+ // Open link in browser if it's opened as a 'foreground-tab'
+ public static newWindow(event: electron.NewWindowEvent) {
+ const url = new window.URL(event.url);
+ if (
+ _.every([
+ url.protocol === 'http:' || url.protocol === 'https:',
+ event.disposition === 'foreground-tab',
+ // Don't open links if they're disabled by the env var
+ !settings.get('disableExternalLinks'),
+ ])
+ ) {
+ electron.shell.openExternal(url.href);
+ }
+ }
+}
diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx
index a6bceb06..3a80dd2c 100644
--- a/lib/gui/app/pages/main/MainPage.tsx
+++ b/lib/gui/app/pages/main/MainPage.tsx
@@ -25,7 +25,7 @@ import { FeaturedProject } from '../../components/featured-project/featured-proj
import FinishPage from '../../components/finish/finish';
import * as ImageSelector from '../../components/image-selector/image-selector';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
-import * as SafeWebview from '../../components/safe-webview/safe-webview';
+import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { SettingsModal } from '../../components/settings/settings';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as flashState from '../../models/flash-state';