mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
Electron 24 - protocol handler (#85)
* added idea and coverage directories under gitignore * electron-24: implemented handlers to process protocol actions * electron-17: implemented use case for opening app if it is not open and handle the protocol url * electron-24: added code and documentation comments * electron-24: added unit tests for the protocol handler * added npm-debug log to gitignore * electron-24: added protocol handler support for windows * electron-24: made changes as per comments on the PR * electron-16: added more comments and further refactoring
This commit is contained in:
parent
4694e5206a
commit
cda34b1d70
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ dist
|
||||
js/preload/_*.js
|
||||
.idea/
|
||||
coverage/
|
||||
npm-debug.log
|
@ -9,10 +9,12 @@ const cmds = keyMirror({
|
||||
badgeDataUrl: null,
|
||||
activate: null,
|
||||
registerBoundsChange: null,
|
||||
registerProtocolHandler: null,
|
||||
checkProtocolAction: null,
|
||||
registerActivityDetection: null,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
cmds: cmds,
|
||||
apiName: 'symphony-api'
|
||||
}
|
||||
};
|
||||
|
129
js/main.js
129
js/main.js
@ -5,8 +5,13 @@ const app = electron.app;
|
||||
const nodeURL = require('url');
|
||||
const squirrelStartup = require('electron-squirrel-startup');
|
||||
const AutoLaunch = require('auto-launch');
|
||||
const { getConfigField } = require('./config.js');
|
||||
const { isMac, isDevEnv } = require('./utils/misc.js');
|
||||
const urlParser = require('url');
|
||||
const {getConfigField} = require('./config.js');
|
||||
const {isMac, isDevEnv} = require('./utils/misc.js');
|
||||
const protocolHandler = require('./protocolHandler');
|
||||
|
||||
// used to check if a url was opened when the app was already open
|
||||
let isAppAlreadyOpen = false;
|
||||
|
||||
// exit early for squirrel installer
|
||||
if (squirrelStartup) {
|
||||
@ -21,15 +26,17 @@ require('./memoryMonitor.js');
|
||||
const windowMgr = require('./windowMgr.js');
|
||||
|
||||
// only allow a single instance of app.
|
||||
const shouldQuit = app.makeSingleInstance(() => {
|
||||
const shouldQuit = app.makeSingleInstance((argv) => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
let mainWin = windowMgr.getMainWindow();
|
||||
if (mainWin) {
|
||||
isAppAlreadyOpen = true;
|
||||
if (mainWin.isMinimized()) {
|
||||
mainWin.restore();
|
||||
}
|
||||
mainWin.focus();
|
||||
}
|
||||
processProtocolAction(argv);
|
||||
});
|
||||
|
||||
// quit if another instance is already running
|
||||
@ -50,52 +57,59 @@ var symphonyAutoLauncher = new AutoLaunch({
|
||||
app.on('ready', getUrlAndOpenMainWindow);
|
||||
|
||||
function getUrlAndOpenMainWindow() {
|
||||
|
||||
processProtocolAction(process.argv);
|
||||
|
||||
isAppAlreadyOpen = true;
|
||||
|
||||
// for dev env allow passing url argument
|
||||
let installMode = false;
|
||||
|
||||
process.argv.some((val) => {
|
||||
|
||||
let flag = '--install';
|
||||
if (val === flag) {
|
||||
installMode = true;
|
||||
getConfigField('launchOnStartup')
|
||||
.then(setStartup);
|
||||
.then(setStartup);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (installMode === false){
|
||||
if (installMode === false) {
|
||||
openMainWindow();
|
||||
}
|
||||
}
|
||||
|
||||
function setStartup(lStartup){
|
||||
if (lStartup === true){
|
||||
function setStartup(lStartup) {
|
||||
if (lStartup === true) {
|
||||
symphonyAutoLauncher.isEnabled()
|
||||
.then(function(isEnabled){
|
||||
if(isEnabled){
|
||||
app.quit();
|
||||
}
|
||||
symphonyAutoLauncher.enable()
|
||||
.then(function (){
|
||||
app.quit();
|
||||
});
|
||||
})
|
||||
} else{
|
||||
symphonyAutoLauncher.isEnabled()
|
||||
.then(function(isEnabled){
|
||||
if(isEnabled){
|
||||
symphonyAutoLauncher.disable()
|
||||
.then(function (){
|
||||
.then(function (isEnabled) {
|
||||
if (isEnabled) {
|
||||
app.quit();
|
||||
});
|
||||
} else{
|
||||
app.quit();
|
||||
}
|
||||
})
|
||||
}
|
||||
symphonyAutoLauncher.enable()
|
||||
.then(function () {
|
||||
app.quit();
|
||||
});
|
||||
})
|
||||
} else {
|
||||
symphonyAutoLauncher.isEnabled()
|
||||
.then(function (isEnabled) {
|
||||
if (isEnabled) {
|
||||
symphonyAutoLauncher.disable()
|
||||
.then(function () {
|
||||
app.quit();
|
||||
});
|
||||
} else {
|
||||
app.quit();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function openMainWindow(){
|
||||
function openMainWindow() {
|
||||
if (isDevEnv) {
|
||||
let url;
|
||||
process.argv.forEach((val) => {
|
||||
@ -131,6 +145,53 @@ function createWin(urlFromConfig) {
|
||||
windowMgr.createMainWindow(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* processes protocol action for windows clients
|
||||
* @param argv {Array} an array of command line arguments
|
||||
*/
|
||||
function processProtocolAction(argv) {
|
||||
|
||||
// In case of windows, we need to handle protocol handler
|
||||
// manually because electron doesn't emit
|
||||
// 'open-url' event on windows
|
||||
if (!(process.platform === 'win32')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let protocolUri;
|
||||
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
|
||||
if (argv[i].startsWith("symphony://")) {
|
||||
protocolUri = argv[i];
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (protocolUri) {
|
||||
|
||||
const parsedURL = urlParser.parse(protocolUri);
|
||||
|
||||
if (!parsedURL.protocol || !parsedURL.slashes) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleProtocolAction(protocolUri);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function handleProtocolAction(uri) {
|
||||
if (!isAppAlreadyOpen) {
|
||||
// app is opened by the protocol url, cache the protocol url to be used later
|
||||
protocolHandler.setProtocolUrl(uri);
|
||||
} else {
|
||||
// app is already open, so, just trigger the protocol action method
|
||||
protocolHandler.processProtocolAction(uri);
|
||||
}
|
||||
}
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
@ -146,3 +207,15 @@ app.on('activate', function () {
|
||||
windowMgr.showMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// adds 'symphony' as a protocol
|
||||
// in the system. plist file in macOS
|
||||
// and registry keys in windows
|
||||
app.setAsDefaultProtocolClient('symphony');
|
||||
|
||||
// This event is emitted only on macOS
|
||||
// at this moment, support for windows
|
||||
// is in pipeline (https://github.com/electron/electron/pull/8052)
|
||||
app.on('open-url', function (event, url) {
|
||||
handleProtocolAction(url);
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ const windowMgr = require('./windowMgr.js');
|
||||
const log = require('./log.js');
|
||||
const activityDetection = require('./activityDetection/activityDetection');
|
||||
const badgeCount = require('./badgeCount.js');
|
||||
const protocolHandler = require('./protocolHandler');
|
||||
|
||||
const apiEnums = require('./enums/api.js');
|
||||
const apiCmds = apiEnums.cmds;
|
||||
@ -69,6 +70,15 @@ electron.ipcMain.on(apiName, (event, arg) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.cmd === apiCmds.checkProtocolAction) {
|
||||
protocolHandler.checkProtocolAction();
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.cmd === apiCmds.registerProtocolHandler) {
|
||||
protocolHandler.setProtocolWindow(event.sender);
|
||||
}
|
||||
|
||||
if (arg.cmd === apiCmds.badgeDataUrl && typeof arg.dataUrl === 'string' &&
|
||||
typeof arg.count === 'number') {
|
||||
badgeCount.setDataUrl(arg.dataUrl, arg.count);
|
||||
@ -100,4 +110,4 @@ module.exports = {
|
||||
shouldCheckValidWindow: function (shouldCheck) {
|
||||
checkValidWindow = shouldCheck;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -32,6 +32,13 @@ const throttledSetBadgeCount = throttle(1000, function(count) {
|
||||
});
|
||||
});
|
||||
|
||||
// check to see if the app was opened via a url
|
||||
const checkProtocolAction = function () {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.checkProtocolAction
|
||||
});
|
||||
};
|
||||
|
||||
createAPI();
|
||||
|
||||
// creates API exposed from electron.
|
||||
@ -65,6 +72,13 @@ function createAPI() {
|
||||
throttledSetBadgeCount(count);
|
||||
},
|
||||
|
||||
/**
|
||||
* checks to see if the app was opened from a url.
|
||||
*/
|
||||
checkProtocolAction: function () {
|
||||
checkProtocolAction();
|
||||
},
|
||||
|
||||
/**
|
||||
* provides api similar to html5 Notification, see details
|
||||
* in notify/notifyImpl.js
|
||||
@ -123,6 +137,24 @@ function createAPI() {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* allows JS to register a protocol handler that can be used by the electron main process.
|
||||
* @param protocolHandler {Object} protocolHandler a callback to register the protocol handler
|
||||
*/
|
||||
registerProtocolHandler: function (protocolHandler) {
|
||||
|
||||
if (typeof protocolHandler === 'function') {
|
||||
|
||||
local.processProtocolAction = protocolHandler;
|
||||
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.registerProtocolHandler
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* allows JS to register a activity detector that can be used by electron main process.
|
||||
* @param {Object} activityDetection - function that can be called accepting
|
||||
@ -235,6 +267,18 @@ function createAPI() {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* an event triggered by the main process for processing protocol urls
|
||||
* @type {String} arg - the protocol url
|
||||
*/
|
||||
local.ipcRenderer.on('protocol-action', (event, arg) => {
|
||||
|
||||
if (local.processProtocolAction && arg) {
|
||||
local.processProtocolAction(arg);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function updateOnlineStatus() {
|
||||
local.ipcRenderer.send(apiName, {
|
||||
cmd: apiCmds.isOnline,
|
||||
|
52
js/protocolHandler/index.js
Normal file
52
js/protocolHandler/index.js
Normal file
@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
|
||||
let protocolWindow;
|
||||
let protocolUrl;
|
||||
|
||||
/**
|
||||
* processes a protocol uri
|
||||
* @param {String} uri - the uri opened in the format 'symphony://...'
|
||||
*/
|
||||
function processProtocolAction(uri) {
|
||||
if (protocolWindow && uri && uri.startsWith("symphony://")) {
|
||||
protocolWindow.send('protocol-action', uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the protocol window
|
||||
* @param {Object} win - the renderer window
|
||||
*/
|
||||
function setProtocolWindow(win) {
|
||||
protocolWindow = win;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks to see if the app was opened by a uri
|
||||
*/
|
||||
function checkProtocolAction() {
|
||||
if (protocolUrl) {
|
||||
processProtocolAction(protocolUrl);
|
||||
protocolUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* caches the protocol uri
|
||||
* @param {String} uri - the uri opened in the format 'symphony://...'
|
||||
*/
|
||||
function setProtocolUrl(uri) {
|
||||
protocolUrl = uri;
|
||||
}
|
||||
|
||||
function getProtocolUrl() {
|
||||
return protocolUrl;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processProtocolAction: processProtocolAction,
|
||||
setProtocolWindow: setProtocolWindow,
|
||||
checkProtocolAction: checkProtocolAction,
|
||||
setProtocolUrl: setProtocolUrl,
|
||||
getProtocolUrl: getProtocolUrl
|
||||
};
|
@ -49,8 +49,7 @@ function removeWindowKey(key) {
|
||||
}
|
||||
|
||||
function getParsedUrl(url) {
|
||||
let parsedUrl = nodeURL.parse(url);
|
||||
return parsedUrl;
|
||||
return nodeURL.parse(url);
|
||||
}
|
||||
|
||||
function createMainWindow(initialUrl) {
|
||||
@ -82,7 +81,7 @@ function doCreateMainWindow(initialUrl, initialBounds) {
|
||||
}
|
||||
};
|
||||
|
||||
// set size and postion
|
||||
// set size and position
|
||||
let bounds = initialBounds;
|
||||
|
||||
// if bounds if not fully contained in some display then use default size
|
||||
@ -127,7 +126,7 @@ function doCreateMainWindow(initialUrl, initialBounds) {
|
||||
}
|
||||
|
||||
// content can be cached and will still finish load but
|
||||
// we might not have netowrk connectivity, so warn the user.
|
||||
// we might not have network connectivity, so warn the user.
|
||||
mainWindow.webContents.on('did-finish-load', function () {
|
||||
url = mainWindow.webContents.getURL();
|
||||
|
||||
|
61
tests/ProtocolHandler.test.js
Normal file
61
tests/ProtocolHandler.test.js
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Created by vishwas on 10/05/17.
|
||||
*/
|
||||
const protocolHandler = require('../js/protocolHandler');
|
||||
const electron = require('./__mocks__/electron');
|
||||
|
||||
describe('protocol handler', function () {
|
||||
|
||||
const url = 'symphony://?userId=100001';
|
||||
|
||||
const mainProcess = electron.ipcMain;
|
||||
const protocolWindow = electron.ipcRenderer;
|
||||
|
||||
beforeAll(function () {
|
||||
protocolHandler.setProtocolWindow(protocolWindow);
|
||||
});
|
||||
|
||||
it('process a protocol action', function (done) {
|
||||
|
||||
const spy = jest.spyOn(protocolHandler, 'processProtocolAction');
|
||||
protocolHandler.processProtocolAction(url);
|
||||
expect(spy).toHaveBeenCalledWith(url);
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('protocol url should be undefined by default', function (done) {
|
||||
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
|
||||
done();
|
||||
});
|
||||
|
||||
it('protocol handler open url should be called', function (done) {
|
||||
|
||||
const spy = jest.spyOn(mainProcess, 'send');
|
||||
mainProcess.send('open-url', url);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
it('check protocol action should be called', function (done) {
|
||||
|
||||
const spy = jest.spyOn(protocolHandler, 'checkProtocolAction');
|
||||
const setSpy = jest.spyOn(protocolHandler, 'setProtocolUrl');
|
||||
|
||||
protocolHandler.setProtocolUrl(url);
|
||||
expect(setSpy).toHaveBeenCalledWith(url);
|
||||
|
||||
protocolHandler.checkProtocolAction();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
expect(protocolHandler.getProtocolUrl()).toBeUndefined();
|
||||
|
||||
done();
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -8,20 +8,44 @@ function pathToConfigDir() {
|
||||
return path.join(__dirname, '/../fixtures');
|
||||
}
|
||||
|
||||
// electron app mock...
|
||||
const app = {
|
||||
getAppPath: pathToConfigDir,
|
||||
getPath: function(type) {
|
||||
if (type === 'exe') {
|
||||
return path.join(pathToConfigDir(), '/Symphony.exe');
|
||||
}
|
||||
return pathToConfigDir();
|
||||
},
|
||||
on: function() {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
// simple ipc mocks for render and main process ipc using
|
||||
// nodes' EventEmitter
|
||||
const ipcMain = {
|
||||
on: function(event, cb) {
|
||||
ipcEmitter.on(event, cb);
|
||||
}
|
||||
}
|
||||
},
|
||||
send: function (event, args) {
|
||||
var senderEvent = {
|
||||
sender: {
|
||||
send: function (event, arg) {
|
||||
ipcEmitter.emit(event, arg);
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcEmitter.emit(event, senderEvent, args);
|
||||
},
|
||||
};
|
||||
|
||||
const ipcRenderer = {
|
||||
sendSync: function(event, args) {
|
||||
let listeners = ipcEmitter.listeners(event);
|
||||
if (listeners.length > 0) {
|
||||
let listener = listeners[0];
|
||||
var eventArg = {}
|
||||
var eventArg = {};
|
||||
listener(eventArg, args);
|
||||
return eventArg.returnValue;
|
||||
}
|
||||
@ -34,7 +58,7 @@ const ipcRenderer = {
|
||||
ipcEmitter.emit(event, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcEmitter.emit(event, senderEvent, args);
|
||||
},
|
||||
on: function(eventName, cb) {
|
||||
@ -43,7 +67,7 @@ const ipcRenderer = {
|
||||
removeListener: function(eventName, cb) {
|
||||
ipcEmitter.removeListener(eventName, cb);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
require: jest.genMockFunction(),
|
||||
|
Loading…
Reference in New Issue
Block a user