Recently, we built NeetoRecord, a loom alternative. The desktop application was built using Electron. In a series of blogs, we capture how we built the desktop application and the challenges we ran into. This blog is part 6 of the blog series. You can also read about part 1, part 2, part 3, part 4, part 5, part 7 part 8 and part 9 .
When developing a desktop application, including every feature directly within the app is often unnecessary. Instead, we can offload some tasks such as login/signup to a web application. And from the web app, create deep-links to the desktop app. We can also create shareable links that opens specific content on the app.
In this blog, we are going to discuss how to create deep-links in our Electron application.
A protocol is a custom URL scheme that an application can handle, similar to how
browsers handle protocols like http
, https
, mailto
, or ftp
. Every
operating system supports the handling of custom protocols. We can register an
application as a default handler for a custom protocol with the operating
system. Electron provides a simple API to register a default protocol client.
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient("my-app", process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient("my-app");
}
The code snippet above is a simple example of registering a default protocol
client. In the example, we registered a custom protocol named my-app
with the
operating system. It's important to note that the application must be properly
packaged and installed so that the operating system can correctly handle and
launch the registered app.
We used process.defaultApp
instead of app.isPackaged
because Electron allows
us to run a packaged app using the electron .
command. In such cases, we need
to provide the execution path for the system to recognize the app. However, if
the app is running in normal mode, simply calling
app.setAsDefaultProtocolClient
is sufficient.
Though registering the protocol is simple and the same for every platform, there are some differences when it comes to handling it.
For macOS, we need to listen to the open-url
event.
const handleCustomUrl = url => {
// Handle url
};
app.on("open-url", (event, url) => {
handleCustomUrl(url);
});
On both Windows and Linux, if the application is up and running, a
second-instance
event is emitted instead of open-url
when a protocol request
is received. This means a new instance of our app will be created. If we want to
avoid this behavior and notify the existing instance instead, we'll need to
ensure the app runs as a single instance. We can achieve this by using
requestSingleInstanceLock
.
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
}
The above code ensures that our Windows or Linux app runs as a single instance. If a user attempts to open a new instance, that instance will try to acquire the single instance lock. If it fails, the newly created instance will simply quit.
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on("second-instance", (event, commands, workingDir) => {
handleCustomUrl(commands.pop());
});
}
If we successfully acquire the single instance lock, it means we're running the
first instance of the app. When a user attempts to open another instance,
Electron emits a second-instance
event to the existing instance. The second
argument of this event contains an array of command-line arguments, and we can
retrieve the custom URL from the last item in this array.
We've addressed the case where the app is already running, but what happens if
the app is completely closed and a protocol request is made? On macOS,
there's nothing extra to do; the app will launch, and the open-url
event will
be triggered automatically.
However, for Windows and Linux, the behavior is different. The
second-instance
event won't be triggered since the app is starting for the
first time. Instead, we can retrieve the custom URL from process.argv
, as the
app will start with the protocol URL passed as one of its parameters. To handle
this case, we need to check process.argv
when the app is started.
const handleCustomUrl = url => {
// Handle url
};
app.whenReady().then(() => {
const customUrl = process.argv.find(item => item.startsWith("my-app://"));
if (customUrl) {
handleCustomUrl(customUrl);
}
});
Here, when the app is ready, we look for an item in process.argv
that starts
with our URL scheme (my-app://
), if yes we can confirm that this instance is
started when a protocol request is received.
Great! Here's the complete solution that works across all platforms, whether the app is already running or not.
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient("my-app", process.execPath, [
path.resolve(process.argv[1]),
]);
}
} else {
app.setAsDefaultProtocolClient("my-app");
}
const handleCustomUrl = url => {
// Handle url
};
app.on("open-url", (event, url) => {
handleCustomUrl(url);
});
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on("second-instance", (event, commands, workingDir) => {
handleCustomUrl(commands.pop());
});
}
app.whenReady().then(() => {
const customUrl = process.argv.find(item => item.startsWith("my-app://"));
if (customUrl) {
handleCustomUrl(customUrl);
}
});
On macOS and Linux, this feature is only functional when our app is packaged; it
won't work during development when launching from the command line. To ensure
proper functionality, we must update the macOS Info.plist
file and the Linux
.desktop
file to include the new protocol handler when packaging our app. This
allows the operating system to recognize and handle the custom URLs correctly.
electron-builder
handles this internally when packaging the app; We just need
to configure the electron-builder
accordingly. To learn more about how to
package our app using electron-builder
, check out
this blog.
"build": {
"productName": "NeetoRecord",
"appId": "com.neeto.neetoRecord",
"protocols": {
"name": "my-app-protocol",
"schemes": [
"my-app"
]
},
"win": {...},
"linux": {...},
"mac": {...}
}
We can use the protocols
field for that, give a name, then pass an array of
URL schemes the app supports, and the rest electron-builder
will handle it.
If this blog was helpful, check out our full blog archive.