Each desktop application can register a list of custom protocols it will handle. One application can register multiple handlers, although it is usually not necessary, but can be done for cleaner separation between concerns, like specific views URLs and authentication.
For example, on Windows, you can use ms-settings:display to open display settings directly. You can either enter it in your browser directly, or type start ms-settings:display
in the command line.
On macOS, you can use the following link:
open x-apple.systempreferences:com.apple.preference.universalaccess?Seeing_Display
We can register custom protocols for our applications. It can be used in many scenarios:
Even if you don’t plan on using it a lot, it is a good option to have in order to extend the functionality of your app. There are a lot of applications with custom protocols handlers. For example, on Windows, execute this command in PowerShell:
1
2
3
Get-ChildItem -Path Registry::HKEY_CLASSES_ROOT | Where-Object {
$_.GetValue("URL Protocol") -ne $null
} | Select-Object PSChildName
On macOS, you can try the following:
1
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -dump | grep -A2 -B2 "scheme:"
The list won’t be exhaustive, but it should give you an idea which apps registered custom protocol handlers.
You need to do 2 things:
app.setAsDefaultProtocolClient
with your protocol1
2
3
4
5
6
7
8
9
10
if (process.defaultApp) {
if (process.argv.length >= 2 && process.argv[1]) {
return app.setAsDefaultProtocolClient(protocol, process.execPath, [
path.resolve(process.argv[1]),
])
}
return false
} else {
return app.setAsDefaultProtocolClient(protocol)
}
The tricky part here is that only Windows allows to register it purely during runtime (if the app is not packaged). MacOS/Linux require the app to be properly packaged and installed first; if you already have the app installable, it should work as long as you start your dev app with your main app closed.
For macOS, your Info.plist
should contain this:
1
2
3
4
5
6
7
8
9
10
11
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yourapp.mac.YourApp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>
And on Linux, your .desktop
file will need to have a line with MimeType and which protocols your app can handle:
1
MimeType=x-scheme-handler/yourapp;
There is a good tutorial in the Electron documentation. The most important part is to understand how different OSes treat additional app launches.
On macOS, the OS enforces a single instance of GUI apps, so when it tries to launch the app when one is already active, it will just activate the currently running instance. To pass that information, we receive the open-url
event, and it has the calling URL as the second argument:
If you log your links, please remember to sanitize them
1
2
app.on('open-url', (event, url) => {
log.info('Received deeplink:', sanitizeAuthDeeplink(url))
An important note here is that if your app was not running, you’ll still receive this event, after app.on('ready')
event is fired.
Windows and Linux handle deeplink activation fundamentally different. The default action is to open another instance of your application. If you are wrapping your web application, this is often not what you want, because unlike tabs in a browser, users expect the same desktop app to be synchronized across multiple instances, and it is not trivial to ensure that. You can explicitly prevent it using app.requestSingleInstanceLock():
1
2
3
4
5
6
const singleInstanceLock = isMAS() || app.requestSingleInstanceLock()
if (!singleInstanceLock) {
app.quit()
} else {
// load your app normally
}
Note: Mac App Store (MAS) version can crash on start if you request the lock. See more info in this Electron issue
After you requested a single instance lock, you’ll receive second-instance
event and a list of process arguments, and you’ll need to parse the list to see if there is a deeplink you can handle:
1
2
3
app.on('second-instance', (_, argv) => {
const deeplinkUrl = argv.find((arg) => arg.startsWith('myapp'))
log.info('Received deeplink:', sanitizeAuthDeeplink(deeplinkUrl))
That is not all! In case your app was not running, this event won’t be triggered, which makes sense: this is the first instance, after all. To combat that, you basically need to run the same deeplinking function on app’s startup and pass process.argv
directly.
If you don’t do so, if somebody obtains a deeplink to your application and click on it (e.g. they use a notification which is kept in the Action Center), it will not respect that and open the default view.