Electron app with custom protocol

How to open custom protocol links from external applications in your Electron desktop app.

Goal

Create lightweight desktop application that can open custom protocol links (similar to iTunes itmss:// or Slack slack:// application links).

Note that it is simple to register an Electron application to handle internal custom protocol links which are in the page an Electron application is rendering. It is harder to register an Electron application to handle custom protocol links clicked in other applications. For this we will need to create an installer.

Start

I took the Electron quick start application from the Quick start guide as the base. I placed the changed application in bahmutov/todomvc-electron-test.

If you want to try the application, clone the repository, install its dependencies and run the start script

1
2
3
4
git clone [email protected]:bahmutov/todomvc-electron-test.git
cd todomvc-electron-test
npm install
npm start

Here is a screenshot of the application in action.

TodoMVC

Note that the loaded Electron application has almost NO logic. Instead it loads https://todomvc-express.bahmutov.com/, which is server side rendering TODO MVC application (available at bahmutov/todomvc-express).

For the demo purposes, the application opens the DevTools panel on startup. I also added a small log function that sends the messages to the panel. This comes very handy when the application will be packaged to run without terminal and need to debug the events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// prints given message both in the terminal console and in the DevTools
function devToolsLog(s) {
console.log(s)
if (mainWindow && mainWindow.webContents) {
mainWindow.webContents.executeJavaScript(`console.log("${s}")`)
}
}
function createWindow () {
mainWindow = new BrowserWindow({width: 1000, height: 800})
// just for demo purposes
mainWindow.webContents.openDevTools()
devToolsLog('process args ' + process.argv.join(','))
...
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

The external website bahmutov/todomvc-express includes two links with custom protocol.

1
2
<p>custom todo protocol link <a href="todo2://active">active</a>,
<a href="todo2://completed">completed</a></p>

When this page is opened in our Electron application, and the user clicks on a link <a href="todo2://completed">completed</a>, we want to catch this click and open the actual page https://todomvc-express.bahmutov.com/completed. We can achieve this by registering custom protocol handler inside the Electron main.js right after the main window has been created

1
2
3
4
5
6
7
8
9
10
11
12
const electron = require('electron')
const protocol = electron.protocol
// handles links `todo2://<something>`
const PROTOCOL_PREFIX = 'todo2'
function createWindow () {
mainWindow = new BrowserWindow({width: 1000, height: 800})
protocol.registerHttpProtocol(PROTOCOL_PREFIX, (req, cb) => {
const fullUrl = formFullTodoUrl(req.url)
devToolsLog('full url to open ' + fullUrl)
mainWindow.loadURL(fullUrl)
})
}

You can see the results right away by clicking on active and completed links which lead to same pages as https://todomvc-express.bahmutov.com/active and https://todomvc-express.bahmutov.com/completed.

Custom protocol internal link

In order to register our new Electron application as the handler for custom protocol links, we need to make an installer that would register it. I will use the project electron-builder to package the Electron app. First, let us create "DMG" installer. The settings will be in the "build" config object inside the package.json file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"scripts": {
"start": "electron .",
"dist": "build"
}
,

"build": {
"appId": "",
"mac": {
"category": ""
}
,

"protocols": [{
"name": "todo2",
"role": "Viewer",
"schemes": ["todo2", "todos2"]
}]

}

}

Notice the "protocols" object that registers both "todo2://" and "todos2://" custom protocols. While unnecessary, this shows how to register multiple custom protocols for one application.

Create the "DMG" file using npm run dist command. This creates todo-<version>.dmg file, that you can execute. Once you drag the application into "Applications" folder, it will become the handler for the custom protocol. Opening the custom link todo2://, either by clicking inside a browser, or by simply open todo2://... from the terminal opens the Todo application.

Opening custom link

Clicking on the custom link the browser brings a dialog prompting the user to "Launch the application".

Custom link in Chrome

Making a Windows installer

I want to build a Windows 64bit installer on a MacOSX laptop. See specific instructions. Directions for Linux systems are slightly different.

To build a Windows installer, let us add a new script command "win" and Windows build settings. In order to install custom protocols, I had to use NSIS installer with perMachine: true option.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"scripts": {
"win": "build --win"
}
,

"build": {
"appId": "",
"mac": {
"category": ""
}
,

"protocols": [{
"name": "todo2",
"role": "Viewer",
"schemes": ["todo2", "todos2"]
}]
,

"win": {
"target": "nsis"
}
,

"nsis": {
"perMachine": true
}

}

}

I also added protocol registration as a separate installer script in build/installer.nsh file

1
2
3
4
5
6
7
8
9
10
!macro customInstall
DetailPrint "Register todo2 URI Handler"
DeleteRegKey HKCR "todo2"
WriteRegStr HKCR "todo2" "" "URL:todo2"
WriteRegStr HKCR "todo2" "URL Protocol" ""
WriteRegStr HKCR "todo2\DefaultIcon" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME}"
WriteRegStr HKCR "todo2\shell" "" ""
WriteRegStr HKCR "todo2\shell\Open" "" ""
WriteRegStr HKCR "todo2\shell\Open\command" "" "$INSTDIR\${APP_EXECUTABLE_FILENAME} %1"
!macroend

Running the command downloads the Windows Electron application, but complains because Wine is not found. Install Wine and open the Wine terminal. Then run npm run win command again and it built a "Setup-.exe" file. This file installs on Windows platform and became the handler for "todo2://" links in other applications.

Making installers on CI

I tried making installers on CI using GitLab docker build. Here my sample .gitlab-ci.yml file. First it runs the tests (mostly just a JS linter), then it tries to build an installer. There are some dependencies that need to be installed for Linux environment, see wiki. Instead of installing dependencies in each test job, I recommend using a special Docker image with all necessary dependencies pre-installed.

1
image: electronuserland/electron-builder:wine
stages:
  - test
  - deploy
test:
  stage: test
  cache:
    paths:
      - node_modules/
  script:
    - npm install --quiet
    - npm test

deploy:
  stage: deploy
  cache:
    paths:
      - node_modules/
  script:
    - npm install --quiet
    - npm run dist
    - echo Building Mac .tar.gz installer
    - npm run dist -- --mac tar.gz
    - echo Building Windows installer
    - npm run win
  artifacts:
    paths:
      - dist/mac/*.tar.gz
      - dist/Todo*.exe

Unfortunately building signed DMG installer for Mac OS requires a Mac machine according to this.

Common problems

  • If you get "spawn icns2png ENOENT" ... - install Linux dependencies
1
2
apt-get update
apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils
  • If you get "spawn gtar ENOENT" on Mac, you will need to install gnu-tar
1
brew install gnu-tar
  • If you get "missing WINE" error, open WINE shell and run the build command.
  • If you see Error: spawn mono ENOENT then you need to install Mono, for example using homebrew.
1
brew install mono

Resources