diff --git a/.gitignore b/.gitignore index f5775935..5a9e9734 100644 --- a/.gitignore +++ b/.gitignore @@ -56,5 +56,6 @@ package-lock.json app/certs/*.pem *electron-builder.yml +*dev-app-update.yml config/production/electron-builder.yml config/ diff --git a/README.md b/README.md index d26a1721..1c5aa7d2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Author: [Ganesh Rathinavel](https://www.linkedin.com/in/ganeshrvel 'Ganesh Rathi Requirements: node.js v8 or higher, yarn -Version: 0.8.3 +Version: 0.08.05 yarn cd ./app && yarn diff --git a/app-update.yml b/app-update.yml new file mode 100644 index 00000000..3f42b9df --- /dev/null +++ b/app-update.yml @@ -0,0 +1,3 @@ +owner: ganeshrvel +repo: openmtp +provider: github diff --git a/app/classes/AppUpdate.js b/app/classes/AppUpdate.js new file mode 100644 index 00000000..79ce37f4 --- /dev/null +++ b/app/classes/AppUpdate.js @@ -0,0 +1,193 @@ +'use strict'; + +import { dialog, BrowserWindow, remote } from 'electron'; +import { autoUpdater } from 'electron-updater'; +import { isConnected } from '../utils/isOnline'; +import ElectronProgressbar from 'electron-progressbar'; +import { log } from '../utils/log'; +import { IS_DEV } from '../constants/env'; +import { PATHS } from '../utils/paths'; + +export default class AppUpdate { + constructor() { + this.autoUpdater = autoUpdater; + this.autoUpdater.updateConfigPath = PATHS.appUpdateFile; + this.autoUpdater.autoDownload = false; + + this.forceCheckProgress = null; + this.downloadProgress = null; + this.updateInitFlag = false; + this.updateForceCheckFlag = false; + } + + init() { + if (this.updateInitFlag) { + return; + } + + this.autoUpdater.on('error', error => { + const errorMsg = + error == null ? 'unknown' : (error.stack || error).toString(); + this.forceCheckProgress.setCompleted(); + + dialog.showErrorBox(`Error: ${errorMsg}`); + log.doLog(error); + }); + + this.autoUpdater.on('update-available', () => { + this.forceCheckProgress.setCompleted(); + + dialog.showMessageBox( + { + type: 'info', + title: 'Updates Found', + message: 'New update found. Update now?', + buttons: ['Yes', 'No'] + }, + buttonIndex => { + switch (buttonIndex) { + case 0: + this.autoUpdater.downloadUpdate(); + break; + default: + case 1: + this.setDownloadUpdatesProgress(); + return; + break; + } + } + ); + }); + + this.autoUpdater.on('download-progress', (ev, progress) => { + if (this.downloadProgress.isCompleted()) { + return null; + } + + this.downloadProgress.value = progress.percent; + }); + + this.autoUpdater.on('update-downloaded', () => { + this.downloadProgress.setCompleted(); + + dialog.showMessageBox( + { + title: 'Install Updates', + message: 'Updates downloaded. Application will be quit for update...', + buttons: ['Install'] + }, + buttonIndex => { + switch (buttonIndex) { + case 0: + default: + autoUpdater.quitAndInstall(); + break; + } + } + ); + }); + + this.updateInitFlag = true; + + try { + } catch (e) { + log.error(e, `AppUpdate -> init`); + } + } + + checkForUpdates() { + this.autoUpdater.checkForUpdates(); + } + + forceCheck() { + if (!this.updateForceCheckFlag) { + this.autoUpdater.on('checking-for-update', () => { + this.setCheckUpdatesProgress(); + }); + + this.autoUpdater.on('update-not-available', () => { + this.forceCheckProgress.setCompleted(); + dialog.showMessageBox( + { + title: 'No Updates Found', + message: 'You have the latest version installed.', + buttons: ['Close'] + }, + buttonIndex => { + switch (buttonIndex) { + case 0: + default: + break; + } + } + ); + }); + } + + this.autoUpdater.checkForUpdates(); + this.updateForceCheckFlag = true; + try { + } catch (e) { + log.error(e, `AppUpdate -> forceCheck`); + } + } + + setCheckUpdatesProgress() { + isConnected().then(connected => { + if (!connected) { + dialog.showMessageBox( + { + title: 'Checking For Updates', + message: 'Internet connection is unavailable.', + buttons: ['Close'] + }, + buttonIndex => { + switch (buttonIndex) { + case 0: + default: + return; + break; + } + } + ); + return null; + } + + this.forceCheckProgress = new ElectronProgressbar({ + title: 'Checking For Updates', + text: 'Please wait...', + detail: '' + }); + }); + } + + setDownloadUpdatesProgress() { + isConnected().then(connected => { + if (!connected) { + dialog.showMessageBox( + { + title: 'Downloading Updates', + message: 'Internet connection is unavailable.', + buttons: ['Close'] + }, + buttonIndex => { + switch (buttonIndex) { + case 0: + default: + return; + break; + } + } + ); + return null; + } + + this.downloadProgress = new ElectronProgressbar({ + indeterminate: false, + title: 'Downloading Updates', + text: 'Please wait...', + detail: '' + }); + }); + } +} diff --git a/app/classes/storage.js b/app/classes/Storage.js similarity index 100% rename from app/classes/storage.js rename to app/classes/Storage.js diff --git a/app/classes/boot.js b/app/classes/boot.js index b9b905c4..b345c976 100644 --- a/app/classes/boot.js +++ b/app/classes/boot.js @@ -16,7 +16,7 @@ const { logFile, settingsFile, logFolder } = PATHS; const deviceType = deviceTypeConst.local; const logFileRotationCleanUpThreshold = 60; //days -export default class boot { +export default class Boot { constructor() { this.verifyDirList = [logFolder]; this.verifyFileList = [settingsFile, logFile]; diff --git a/app/containers/App/index.jsx b/app/containers/App/index.jsx index 32c57321..3cefc02c 100644 --- a/app/containers/App/index.jsx +++ b/app/containers/App/index.jsx @@ -11,7 +11,7 @@ import Typography from '@material-ui/core/Typography'; import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles'; import Routes from '../../routing'; -import bootApp from '../../classes/boot'; +import bootApp from '../../classes/Boot'; import { settingsStorage } from '../../utils/storageHelper'; import SettingsDialog from '../Settings'; import { withReducer } from '../../store/reducers/withReducer'; diff --git a/app/main.dev.js b/app/main.dev.js index 94978f90..abc45c38 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -1,26 +1,16 @@ 'use strict'; -/** - * This module executes inside of electron's main process. You can start - * electron renderer process from here and communicate with the other processes - * through IPC. - * - * When running `yarn build` or `yarn build-main`, this file is compiled to - * `./app/main.prod.js` using webpack. This gives us some performance wins. - * - */ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, ipcMain } from 'electron'; import MenuBuilder from './menu'; import { log } from './utils/log'; -import { IS_PROD, DEBUG_PROD, IS_DEV } from './constants/env'; +import { DEBUG_PROD, IS_DEV } from './constants/env'; +import AppUpdate from './classes/AppUpdate'; let mainWindow = null; const isSingleInstance = app.requestSingleInstanceLock(); +const autoAppUpdate = new AppUpdate(); -if (IS_PROD) { - const sourceMapSupport = require('source-map-support'); - sourceMapSupport.install(); -} +/*autoAppUpdate.init();*/ if (IS_DEV || DEBUG_PROD) { require('electron-debug')(); @@ -99,7 +89,7 @@ const createWindow = async () => { mainWindow = null; }); - const menuBuilder = new MenuBuilder(mainWindow); + const menuBuilder = new MenuBuilder({ mainWindow, autoAppUpdate }); menuBuilder.buildMenu(); } catch (e) { log.error(e, `main.dev -> createWindow`); diff --git a/app/menu.js b/app/menu.js index 775be6b0..62ba965c 100644 --- a/app/menu.js +++ b/app/menu.js @@ -34,11 +34,13 @@ const fireReportBugs = () => { }; export default class MenuBuilder { - constructor(mainWindow) { + constructor({ mainWindow, autoAppUpdate }) { this.mainWindow = mainWindow; + this.autoAppUpdate = autoAppUpdate; } buildMenu() { + this.setupDevelopmentEnvironment(); if ( process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true' @@ -108,6 +110,13 @@ export default class MenuBuilder { selector: 'orderFrontStandardAboutPanel:' }, { type: 'separator' }, + { + label: 'Check For Updates', + click: () => { + this.autoAppUpdate.forceCheck(); + } + }, + { type: 'separator' }, { label: 'Hide the OpenMTP', accelerator: 'Command+H', @@ -302,6 +311,12 @@ export default class MenuBuilder { { label: 'Help', submenu: [ + { + label: 'Check For Updates', + click: () => { + this.autoAppUpdate.forceCheck(); + } + }, { label: 'Send Error Logs', click: () => { diff --git a/app/utils/isOnline.js b/app/utils/isOnline.js new file mode 100644 index 00000000..00d559c3 --- /dev/null +++ b/app/utils/isOnline.js @@ -0,0 +1,19 @@ +'use strict'; +import Promise from 'bluebird'; +import { log } from '../utils/log'; + +export const isConnected = () => { + try { + return new Promise((resolve, reject) => { + require('dns').lookup('github.com', function(err) { + if (err && err.code === 'ENOTFOUND') { + resolve(false); + return null; + } + resolve(true); + }); + }); + } catch (e) { + log.error(e, `isOnline -> isConnected`); + } +}; diff --git a/app/utils/paths.js b/app/utils/paths.js index 737d3974..d9101250 100644 --- a/app/utils/paths.js +++ b/app/utils/paths.js @@ -16,6 +16,7 @@ const logFileName = IS_DEV const logFolder = path.join(profileFolder, `./logs`); const logFile = path.join(logFolder, `./${productName}-${logFileName}`); const settingsFile = path.join(profileFolder, `./settings.json`); +const appUpdateFile = path.join(root, `./app-update.yml`); export const PATHS = { root: path.resolve(root), @@ -26,7 +27,8 @@ export const PATHS = { profileFolder: path.resolve(profileFolder), logFolder: path.resolve(logFolder), logFile: path.resolve(logFile), - settingsFile: path.resolve(settingsFile) + settingsFile: path.resolve(settingsFile), + appUpdateFile: path.resolve(appUpdateFile) }; export const pathUp = filePath => { diff --git a/app/utils/storageHelper.js b/app/utils/storageHelper.js index ecb7318c..94d20b7b 100644 --- a/app/utils/storageHelper.js +++ b/app/utils/storageHelper.js @@ -1,7 +1,7 @@ 'use strict'; import { PATHS } from './paths'; -import Storage from '../classes/storage'; +import Storage from '../classes/Storage'; const { settingsFile } = PATHS; diff --git a/package.json b/package.json index 5ff8f89c..9860e311 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "start": "cross-env NODE_ENV=production electron ./app/", "start-main-dev": "cross-env HOT=1 NODE_ENV=development electron -r babel-register ./app/main.dev.js", "start-renderer-dev": "cross-env NODE_ENV=development node --trace-warnings -r babel-register ./node_modules/webpack-dev-server/bin/webpack-dev-server --config webpack/config.renderer.dev.js", - "publish:mac": "yarn build && electron-builder build --mac --config=config/deploy/electron-builder.yml --publish always" + "publish-mac": "yarn build && electron-builder build --mac --config=config/deploy/electron-builder.yml --publish always" }, "browserslist": "electron 3.0", "build": { @@ -163,6 +163,8 @@ "classnames": "^2.2.6", "devtron": "^1.4.0", "electron-debug": "^2.0.0", + "electron-progressbar": "^1.1.0", + "electron-updater": "^4.0.6", "history": "^4.7.2", "immutable": "^3.8.2", "junk": "^2.1.0",