MEW-01-009 & MEW-01-010: Electron security fixes (#910)
* Handle opening of external links in electron. Minor refactor of window code. * Convert updates from in-app modal to electron dialogs. Remove in-app code and preload bridge. * Refine new window blocking. Re-enable tsconfig to look at electron-app. * Clean up shared * Whoops, wrong protocol format
This commit is contained in:
parent
2e49d4718b
commit
df52521c17
|
@ -1,61 +0,0 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
@keyframes new-update-popin {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes new-update-glow {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
},
|
||||
80%, 100% {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
.Version {
|
||||
position: relative;
|
||||
|
||||
&-text {
|
||||
&.has-update:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&-new {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: -12px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
background: $brand-warning;
|
||||
animation: new-update-popin 500ms ease 1;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 3px $brand-warning;
|
||||
animation: new-update-glow 1200ms ease infinite;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +1,6 @@
|
|||
import React from 'react';
|
||||
import { VERSION } from 'config/data';
|
||||
import UpdateModal, { UpdateInfo } from 'components/UpdateModal';
|
||||
import { addListener } from 'utils/electron';
|
||||
import EVENTS from 'shared/electronEvents';
|
||||
import './Version.scss';
|
||||
|
||||
interface State {
|
||||
updateInfo: UpdateInfo | null;
|
||||
isModalOpen: boolean;
|
||||
}
|
||||
const Version: React.SFC<{}> = () => <div className="Version">v{VERSION}</div>;
|
||||
|
||||
export default class Version extends React.Component<{}, State> {
|
||||
public state: State = {
|
||||
updateInfo: null,
|
||||
isModalOpen: false
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
addListener(EVENTS.UPDATE.UPDATE_AVAILABLE, updateInfo => {
|
||||
this.setState({ updateInfo });
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { updateInfo, isModalOpen } = this.state;
|
||||
return (
|
||||
<div className="Version">
|
||||
<span className={`Version-text ${updateInfo ? 'has-update' : ''}`} onClick={this.openModal}>
|
||||
v{VERSION}
|
||||
</span>
|
||||
{updateInfo && (
|
||||
<span>
|
||||
<span className="Version-new" />
|
||||
<UpdateModal
|
||||
isOpen={isModalOpen}
|
||||
updateInfo={updateInfo}
|
||||
handleClose={this.closeModal}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private openModal = () => this.setState({ isModalOpen: true });
|
||||
private closeModal = () => this.setState({ isModalOpen: false });
|
||||
}
|
||||
export default Version;
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.UpdateModal {
|
||||
cursor: default;
|
||||
@media (min-width: 680px) {
|
||||
min-width: 680px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: $space-xs;
|
||||
}
|
||||
|
||||
&-date {
|
||||
font-size: $font-size-small;
|
||||
color: $gray;
|
||||
padding-bottom: $space-sm;
|
||||
border-bottom: 1px solid $gray-lighter;
|
||||
}
|
||||
|
||||
&-downloader {
|
||||
padding: 50px 30px 80px;
|
||||
text-align: center;
|
||||
|
||||
&-bar {
|
||||
position: relative;
|
||||
margin: 0 20px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
background: $gray-lighter;
|
||||
margin-bottom: $space-sm;
|
||||
|
||||
&-inner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
border-radius: 4px;
|
||||
background: $brand-primary;
|
||||
transition: width 100ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
color: $gray-light;
|
||||
font-size: $font-size-xs;
|
||||
|
||||
&-bit {
|
||||
&:after {
|
||||
display: inline-block;
|
||||
content: "•";
|
||||
padding: 0 $space-xs;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import { Spinner, NewTabLink } from 'components/ui';
|
||||
import Modal, { IButton } from 'components/ui/Modal';
|
||||
import { addListener, sendEvent } from 'utils/electron';
|
||||
import EVENTS from 'shared/electronEvents';
|
||||
import { bytesToHuman } from 'utils/formatters';
|
||||
import './UpdateModal.scss';
|
||||
|
||||
export interface UpdateInfo {
|
||||
version: string;
|
||||
sha512: string;
|
||||
releaseDate: string;
|
||||
releaseName: string;
|
||||
releaseNotes: string;
|
||||
}
|
||||
|
||||
export interface DownloadProgress {
|
||||
bytesPerSecond: number;
|
||||
percent: number;
|
||||
transferred: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
updateInfo: UpdateInfo;
|
||||
showNotification: TShowNotification;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isDownloading: boolean;
|
||||
downloadProgress: DownloadProgress | null;
|
||||
}
|
||||
|
||||
class UpdateModal extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
isDownloading: false,
|
||||
downloadProgress: null
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
addListener(EVENTS.UPDATE.UPDATE_DOWNLOADED, () => {
|
||||
sendEvent(EVENTS.UPDATE.QUIT_AND_INSTALL);
|
||||
});
|
||||
addListener(EVENTS.UPDATE.DOWNLOAD_PROGRESS, downloadProgress => {
|
||||
this.setState({ downloadProgress });
|
||||
});
|
||||
addListener(EVENTS.UPDATE.ERROR, err => {
|
||||
console.error('Update failed:', err);
|
||||
this.setState({ isDownloading: false });
|
||||
this.props.showNotification(
|
||||
'danger',
|
||||
<span>
|
||||
Update could not be downloaded, please visit{' '}
|
||||
<NewTabLink href="https://github.com/MyEtherWallet/MyEtherWallet/releases">
|
||||
our github
|
||||
</NewTabLink>{' '}
|
||||
to download the latest release
|
||||
</span>,
|
||||
Infinity
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { isOpen, updateInfo, handleClose } = this.props;
|
||||
const { isDownloading, downloadProgress } = this.state;
|
||||
const buttons: IButton[] | undefined = downloadProgress
|
||||
? undefined
|
||||
: [
|
||||
{
|
||||
text: <span>{isDownloading && <Spinner />} Download Update</span>,
|
||||
type: 'primary',
|
||||
onClick: this.downloadUpdate,
|
||||
disabled: isDownloading
|
||||
},
|
||||
{
|
||||
text: 'Close',
|
||||
type: 'default',
|
||||
onClick: handleClose
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={`Update Information`}
|
||||
handleClose={handleClose}
|
||||
buttons={buttons}
|
||||
>
|
||||
<div className="UpdateModal">
|
||||
{downloadProgress ? (
|
||||
<div className="UpdateModal-downloader">
|
||||
<h3 className="UpdateModal-downloader-title">Downloading...</h3>
|
||||
<div className="UpdateModal-downloader-bar">
|
||||
<div
|
||||
className="UpdateModal-downloader-bar-inner"
|
||||
style={{
|
||||
width: `${downloadProgress.percent}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="UpdateModal-downloader-info">
|
||||
<span className="UpdateModal-downloader-info-bit">
|
||||
Downloaded {downloadProgress.percent.toFixed(1)}%
|
||||
</span>
|
||||
<span className="UpdateModal-downloader-info-bit">
|
||||
{bytesToHuman(downloadProgress.bytesPerSecond)}/s
|
||||
</span>
|
||||
<span className="UpdateModal-downloader-info-bit">
|
||||
Total Size {bytesToHuman(downloadProgress.total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h1 className="UpdateModal-title">{updateInfo.releaseName}</h1>
|
||||
<div className="UpdateModal-date">{moment(updateInfo.releaseDate).format('LL')}</div>
|
||||
<div
|
||||
className="UpdateModal-content"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: updateInfo.releaseNotes
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
private downloadUpdate = () => {
|
||||
this.setState({ isDownloading: true });
|
||||
sendEvent('UPDATE:download-update');
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(undefined, { showNotification })(UpdateModal);
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import { openInBrowser } from 'utils/electron';
|
||||
|
||||
interface AAttributes {
|
||||
charset?: string;
|
||||
|
@ -40,17 +39,11 @@ export class NewTabLink extends React.Component<NewTabLinkProps> {
|
|||
public render() {
|
||||
const { content, children, ...rest } = this.props;
|
||||
return (
|
||||
<a target="_blank" rel="noopener noreferrer" onClick={this.handleClick} {...rest}>
|
||||
<a target="_blank" rel="noopener noreferrer" {...rest}>
|
||||
{content || children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
private handleClick(ev: React.MouseEvent<HTMLAnchorElement>) {
|
||||
if (openInBrowser(ev.currentTarget.href)) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NewTabLink;
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// Handles integrations with Electron. Wherever possible, should stub out
|
||||
// behavior with noop's if not in the Electron environment, to simplify code.
|
||||
import { ElectronBridgeFunctions } from 'shared/electronBridge';
|
||||
const bridge: ElectronBridgeFunctions | null = (window as any).electronBridge;
|
||||
|
||||
export const addListener: ElectronBridgeFunctions['addListener'] = (event, cb) => {
|
||||
if (bridge && bridge.addListener) {
|
||||
// @ts-ignore unused ev
|
||||
bridge.addListener(event, (ev, data) => cb(data));
|
||||
}
|
||||
};
|
||||
|
||||
export const sendEvent: ElectronBridgeFunctions['sendEvent'] = (event, data) => {
|
||||
if (bridge && bridge.sendEvent) {
|
||||
bridge.sendEvent(event, data);
|
||||
}
|
||||
};
|
||||
|
||||
export const openInBrowser: ElectronBridgeFunctions['openInBrowser'] = url => {
|
||||
if (bridge && bridge.openInBrowser) {
|
||||
bridge.openInBrowser(url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -1,63 +1,5 @@
|
|||
import { app, BrowserWindow, Menu } from 'electron';
|
||||
import * as path from 'path';
|
||||
import updater from './updater';
|
||||
import MENU from './menu';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// Global reference to mainWindow
|
||||
// Necessary to prevent window from being garbage collected
|
||||
let mainWindow;
|
||||
|
||||
function createMainWindow() {
|
||||
// Construct new BrowserWindow
|
||||
const window = new BrowserWindow({
|
||||
title: 'MyEtherWallet',
|
||||
backgroundColor: '#fbfbfb',
|
||||
width: 1220,
|
||||
height: 800,
|
||||
minWidth: 320,
|
||||
minHeight: 400,
|
||||
// TODO - Implement styles for custom title bar in components/ui/TitleBar.scss
|
||||
// frame: false,
|
||||
// titleBarStyle: 'hidden',
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: false,
|
||||
preload: path.resolve(__dirname, 'preload.js')
|
||||
}
|
||||
});
|
||||
|
||||
const url = isDevelopment
|
||||
? `http://localhost:${process.env.HTTPS ? 3443 : 3000}`
|
||||
: `file://${__dirname}/index.html`;
|
||||
window.loadURL(url);
|
||||
|
||||
window.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
window.webContents.on('devtools-opened', () => {
|
||||
window.focus();
|
||||
setImmediate(() => {
|
||||
window.focus();
|
||||
});
|
||||
});
|
||||
|
||||
if (isDevelopment) {
|
||||
window.webContents.on('did-fail-load', () => {
|
||||
setTimeout(() => {
|
||||
if (window && window.webContents) {
|
||||
window.webContents.reload();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(MENU));
|
||||
|
||||
return window;
|
||||
}
|
||||
import { app } from 'electron';
|
||||
import getWindow from './window';
|
||||
|
||||
// Quit application when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
|
@ -71,16 +13,10 @@ app.on('window-all-closed', () => {
|
|||
app.on('activate', () => {
|
||||
// On macOS it is common to re-create a window
|
||||
// even after all windows have been closed
|
||||
if (mainWindow === null) {
|
||||
mainWindow = createMainWindow();
|
||||
updater(app, mainWindow);
|
||||
}
|
||||
getWindow();
|
||||
});
|
||||
|
||||
// Create main BrowserWindow when electron is ready
|
||||
app.on('ready', () => {
|
||||
mainWindow = createMainWindow();
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
updater(app, mainWindow);
|
||||
});
|
||||
getWindow();
|
||||
});
|
||||
|
|
|
@ -1,97 +1,144 @@
|
|||
import { App, BrowserWindow, ipcMain } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import EVENTS from '../../shared/electronEvents';
|
||||
import { app, dialog, BrowserWindow } from 'electron';
|
||||
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
||||
import TEST_RELEASE from './testrelease.json';
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
// Set to 'true' if you want to test update behavior. Requires a recompile.
|
||||
const shouldMockUpdate = false && process.env.NODE_ENV !== 'production';
|
||||
const shouldMockUpdateError = false && process.env.NODE_ENV !== 'production';
|
||||
let hasRunUpdater = false;
|
||||
let hasStartedUpdating = false;
|
||||
|
||||
enum AutoUpdaterEvents {
|
||||
CHECKING_FOR_UPDATE = 'checking-for-update',
|
||||
UPDATE_NOT_AVAILABLE = 'update-not-available',
|
||||
UPDATE_AVAILABLE = 'update-available',
|
||||
DOWNLOAD_PROGRESS = 'download-progress',
|
||||
UPDATE_DOWNLOADED = 'update-downloaded',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
export default (app: App, window: BrowserWindow) => {
|
||||
// Set to 'true' if you want to test update behavior. Requires a recompile.
|
||||
const shouldMockUpdate = true && process.env.NODE_ENV !== 'production';
|
||||
export default function(mainWindow: BrowserWindow) {
|
||||
if (hasRunUpdater) {
|
||||
return;
|
||||
}
|
||||
hasRunUpdater = true;
|
||||
|
||||
// Report update status
|
||||
autoUpdater.on(AutoUpdaterEvents.CHECKING_FOR_UPDATE, () => {
|
||||
window.webContents.send(EVENTS.UPDATE.CHECKING_FOR_UPDATE);
|
||||
});
|
||||
|
||||
autoUpdater.on(AutoUpdaterEvents.UPDATE_NOT_AVAILABLE, () => {
|
||||
window.webContents.send(EVENTS.UPDATE.UPDATE_NOT_AVAILABLE);
|
||||
});
|
||||
|
||||
autoUpdater.on(AutoUpdaterEvents.UPDATE_AVAILABLE, info => {
|
||||
window.webContents.send(EVENTS.UPDATE.UPDATE_AVAILABLE, info);
|
||||
autoUpdater.on(AutoUpdaterEvents.UPDATE_AVAILABLE, (info: UpdateInfo) => {
|
||||
dialog.showMessageBox(
|
||||
{
|
||||
type: 'question',
|
||||
buttons: ['Yes, start downloading', 'Maybe later'],
|
||||
title: `An Update is Available (v${info.version})`,
|
||||
message: `An Update is Available (v${info.version})`,
|
||||
detail:
|
||||
'A new version has been released. Would you like to start downloading the update? You will be notified when the download is finished.'
|
||||
},
|
||||
response => {
|
||||
if (response === 0) {
|
||||
if (shouldMockUpdate) {
|
||||
mockDownload();
|
||||
} else {
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
hasStartedUpdating = true;
|
||||
});
|
||||
|
||||
autoUpdater.on(AutoUpdaterEvents.DOWNLOAD_PROGRESS, progress => {
|
||||
window.webContents.send(EVENTS.UPDATE.DOWNLOAD_PROGRESS, progress);
|
||||
mainWindow.setTitle(`MyEtherWallet (Downloading update... ${Math.round(progress.percent)}%)`);
|
||||
mainWindow.setProgressBar(progress.percent / 100);
|
||||
});
|
||||
|
||||
autoUpdater.on(AutoUpdaterEvents.UPDATE_DOWNLOADED, () => {
|
||||
window.webContents.send(EVENTS.UPDATE.UPDATE_DOWNLOADED);
|
||||
resetWindowFromUpdates(mainWindow);
|
||||
dialog.showMessageBox(
|
||||
{
|
||||
type: 'question',
|
||||
buttons: ['Yes, restart now', 'Maybe later'],
|
||||
title: 'Update Has Been Downloaded',
|
||||
message: 'Download complete!',
|
||||
detail:
|
||||
'The new version of MyEtherWallet has finished downloading. Would you like to restart to complete the installation?'
|
||||
},
|
||||
response => {
|
||||
if (response === 0) {
|
||||
if (shouldMockUpdate) {
|
||||
app.quit();
|
||||
} else {
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
autoUpdater.on(AutoUpdaterEvents.ERROR, (err, msg) => {
|
||||
autoUpdater.on(AutoUpdaterEvents.ERROR, (err: Error) => {
|
||||
console.error('Update failed with an error');
|
||||
console.error(err);
|
||||
window.webContents.send(EVENTS.UPDATE.ERROR, msg);
|
||||
|
||||
// If they haven't started updating yet, just fail silently
|
||||
if (!hasStartedUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetWindowFromUpdates(mainWindow);
|
||||
dialog.showErrorBox(
|
||||
'Downloading Update has Failed',
|
||||
`The update could not be downloaded. Restart the app and try again later, or manually install the new update at https://github.com/MyEtherWallet/MyEtherWallet/releases\n\n(${
|
||||
err.name
|
||||
}: ${err.message})`
|
||||
);
|
||||
});
|
||||
|
||||
// Kick off the check
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
|
||||
// Listen for restart request
|
||||
ipcMain.on(EVENTS.UPDATE.DOWNLOAD_UPDATE, () => {
|
||||
if (shouldMockUpdate) {
|
||||
mockDownload(window);
|
||||
} else {
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(EVENTS.UPDATE.QUIT_AND_INSTALL, () => {
|
||||
if (shouldMockUpdate) {
|
||||
app.quit();
|
||||
} else {
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
|
||||
// Simulate a test release
|
||||
if (shouldMockUpdate) {
|
||||
mockUpdateCheck(window);
|
||||
mockUpdateCheck();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resetWindowFromUpdates(window: BrowserWindow) {
|
||||
window.setTitle('MyEtherWallet');
|
||||
window.setProgressBar(-1); // Clears progress bar
|
||||
}
|
||||
|
||||
// Mock functions for dev testing
|
||||
function mockUpdateCheck(window: BrowserWindow) {
|
||||
window.webContents.send(EVENTS.UPDATE.CHECKING_FOR_UPDATE);
|
||||
function mockUpdateCheck() {
|
||||
autoUpdater.emit(AutoUpdaterEvents.CHECKING_FOR_UPDATE);
|
||||
|
||||
setTimeout(() => {
|
||||
window.webContents.send(EVENTS.UPDATE.UPDATE_AVAILABLE, TEST_RELEASE);
|
||||
autoUpdater.emit(AutoUpdaterEvents.UPDATE_AVAILABLE, TEST_RELEASE);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function mockDownload(window: BrowserWindow) {
|
||||
for (let i = 0; i < 101; i++) {
|
||||
function mockDownload() {
|
||||
for (let i = 0; i < 11; i++) {
|
||||
setTimeout(() => {
|
||||
if (i >= 5 && shouldMockUpdateError) {
|
||||
if (i === 5) {
|
||||
autoUpdater.emit(
|
||||
AutoUpdaterEvents.ERROR,
|
||||
new Error('Test error, nothing actually failed')
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const total = 150000000;
|
||||
window.webContents.send(EVENTS.UPDATE.DOWNLOAD_PROGRESS, {
|
||||
bytesPerSecond: Math.round(Math.random() * 100000),
|
||||
percent: i,
|
||||
autoUpdater.emit(AutoUpdaterEvents.DOWNLOAD_PROGRESS, {
|
||||
bytesPerSecond: Math.round(Math.random() * 100000000),
|
||||
percent: i * 10,
|
||||
transferred: total / i,
|
||||
total
|
||||
});
|
||||
|
||||
if (i === 100) {
|
||||
window.webContents.send(EVENTS.UPDATE.UPDATE_DOWNLOADED);
|
||||
if (i === 10) {
|
||||
autoUpdater.emit(AutoUpdaterEvents.UPDATE_DOWNLOADED);
|
||||
}
|
||||
}, 50 * i);
|
||||
}, 500 * i);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import { BrowserWindow, Menu, shell } from 'electron';
|
||||
import { URL } from 'url';
|
||||
import MENU from './menu';
|
||||
import updater from './updater';
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// Cached reference, preventing recreations
|
||||
let window;
|
||||
|
||||
// Construct new BrowserWindow
|
||||
export default function getWindow() {
|
||||
if (window) {
|
||||
return window;
|
||||
}
|
||||
|
||||
window = new BrowserWindow({
|
||||
title: 'MyEtherWallet',
|
||||
backgroundColor: '#fbfbfb',
|
||||
width: 1220,
|
||||
height: 800,
|
||||
minWidth: 320,
|
||||
minHeight: 400,
|
||||
// TODO - Implement styles for custom title bar in components/ui/TitleBar.scss
|
||||
// frame: false,
|
||||
// titleBarStyle: 'hidden',
|
||||
webPreferences: {
|
||||
devTools: true,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.HTTPS ? '3443' : '3000';
|
||||
const appUrl = isDevelopment ? `http://localhost:${port}` : `file://${__dirname}/index.html`;
|
||||
window.loadURL(appUrl);
|
||||
|
||||
window.on('closed', () => {
|
||||
window = null;
|
||||
});
|
||||
|
||||
window.webContents.on('new-window', (ev, urlStr) => {
|
||||
// Kill all new window requests by default
|
||||
ev.preventDefault();
|
||||
|
||||
// Only allow HTTPS urls to actually be opened
|
||||
const url = new URL(urlStr);
|
||||
if (url.protocol === 'https:') {
|
||||
shell.openExternal(urlStr);
|
||||
} else {
|
||||
console.warn(`Blocked request to open new window '${urlStr}', only HTTPS links are allowed`);
|
||||
}
|
||||
});
|
||||
|
||||
window.webContents.on('did-finish-load', () => {
|
||||
updater(window);
|
||||
});
|
||||
|
||||
window.webContents.on('devtools-opened', () => {
|
||||
window.focus();
|
||||
setImmediate(() => {
|
||||
window.focus();
|
||||
});
|
||||
});
|
||||
|
||||
if (isDevelopment) {
|
||||
window.webContents.on('did-fail-load', () => {
|
||||
setTimeout(() => {
|
||||
if (window && window.webContents) {
|
||||
window.webContents.reload();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(MENU));
|
||||
|
||||
return window;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
// Selectively expose node integration, since all node integrations are
|
||||
// disabled by default for security purposes.
|
||||
import { ipcRenderer, shell } from 'electron';
|
||||
import { ElectronBridgeFunctions } from '../shared/electronBridge';
|
||||
const win = window as any;
|
||||
|
||||
const functions: ElectronBridgeFunctions = {
|
||||
addListener(event, cb) {
|
||||
ipcRenderer.on(event, cb);
|
||||
},
|
||||
sendEvent(event, data) {
|
||||
ipcRenderer.send(event, data);
|
||||
},
|
||||
openInBrowser(url) {
|
||||
return shell.openExternal(url);
|
||||
}
|
||||
};
|
||||
|
||||
win.electronBridge = functions;
|
|
@ -10,8 +10,7 @@
|
|||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/jest_config/__mocks__/fileMock.ts",
|
||||
"\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts",
|
||||
"\\.worker.ts":"<rootDir>/jest_config/__mocks__/workerMock.js",
|
||||
"^shared(.*)$": "<rootDir>/shared$1"
|
||||
"\\.worker.ts":"<rootDir>/jest_config/__mocks__/workerMock.js"
|
||||
},
|
||||
"testPathIgnorePatterns": ["<rootDir>/common/config"],
|
||||
"setupFiles": [
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<rootDir>/jest_config/__mocks__/fileMock.ts",
|
||||
"\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts",
|
||||
"^shared(.*)$": "<rootDir>/shared$1"
|
||||
"\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts"
|
||||
},
|
||||
"testPathIgnorePatterns": ["<rootDir>/common/config"],
|
||||
"setupFiles": [
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
// Provide typescript definitions / mappings between `electron-app/preload.ts`
|
||||
// and 'common/utils/electron.ts'
|
||||
export type ElectronBridgeCallback = (data?: any) => void;
|
||||
|
||||
export interface ElectronBridgeFunctions {
|
||||
addListener(event: string, cb: ElectronBridgeCallback);
|
||||
sendEvent(event: string, data?: any);
|
||||
openInBrowser(url: string): boolean;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
export default {
|
||||
UPDATE: {
|
||||
CHECKING_FOR_UPDATE: 'UPDATE:checking-for-update',
|
||||
UPDATE_NOT_AVAILABLE: 'UPDATE:update-not-available',
|
||||
UPDATE_AVAILABLE: 'UPDATE:update-available',
|
||||
DOWNLOAD_PROGRESS: 'UPDATE:download-progress',
|
||||
UPDATE_DOWNLOADED: 'UPDATE:update-downloaded',
|
||||
ERROR: 'UPDATE:error',
|
||||
DOWNLOAD_UPDATE: 'UPDATE:download-update',
|
||||
QUIT_AND_INSTALL: 'UPDATE:quit-and-install'
|
||||
}
|
||||
};
|
|
@ -23,7 +23,7 @@
|
|||
},
|
||||
"include": [
|
||||
"./common/",
|
||||
"./electron/",
|
||||
"./electron-app/",
|
||||
"spec",
|
||||
"./node_modules/types-rlp/index.d.ts"
|
||||
],
|
||||
|
|
|
@ -8,8 +8,7 @@ const makeConfig = require('./makeConfig');
|
|||
const electronConfig = {
|
||||
target: 'electron-main',
|
||||
entry: {
|
||||
main: path.join(config.path.electron, 'main/index.ts'),
|
||||
preload: path.join(config.path.electron, 'preload.ts')
|
||||
main: path.join(config.path.electron, 'main/index.ts')
|
||||
},
|
||||
module: {
|
||||
rules: [config.typescriptRule]
|
||||
|
|
Loading…
Reference in New Issue