Explorations and Ramblings in Functional Programming, Mobile and General Development in Typescript *
Deep-Linking in Electron
open in app ? Hell yeah
Deep-Linking in React Native
open in app ? Hell yeah (part 2)
Boosting Peformance in React Native
React Native isn't fast- said noone, ever
Turntable:Exploring OAuth in desktop applications
Research and lessons learned from implementing OAuth in an electron app
what is deep linking ? Deep Linking is the ability to trigger functionality within application from external sources sometimes with data that can enable that functionality to be carried out
Deep Linking in applications (both mobile and desktop) is one of the lesser documented development process on the internet despite many popular applications making use of this technique, from Instagram to VSCode they all have instance of deep linking in them but articles covering the how are few and far between. Which is why this one exists along with it's companion piece for React Native, to help guide developers to implementing Deep Linking without having to dig through very verbose articles that beat around the bush or present outdated techniques.
There are 3 key steps involved in getting running with deep linking in electron applications and I do my best in this article, to guide you through these steps in an understandable and cohesive manner
desktop applications that are capable of being launched by external applications need to register a custom protocol that it can listen for and can be emitted by source applications to ensure it is activated. Think of it like your web protocols, like http or https, But for your app specifically and only your app can intercept it. protocols you'll see in the wild usually follow this pattern
my-app://
setting up a protocol in electron is quite straighforward and involves a single line of code
app.setDefaultProtocolClient("my-app",process.execPath,[path.resolve(process.argv[1])]])
In standard documentation you'd probably find something like this and while it is quite verbose, it's not a far cry from the example above
if(process.defaultApp){
if(process.argv.length>=2){
app.setDefaultProtocolClient("my-app",process.execPath,[path.resolve(process.argv[1])]])
}
}else{
app.setDefaultProtocolClient("my-app")
}
lines 1-5 are a precaution we take in development to ensure that our protocol is registered because, depending on the electron tooling you're using, most things are built and output before the application window is launched. This, is why process.execPath and path.resolve are necessary, so it can point to the appropriate main.ts/entry.ts file where our protocol handler(s) will live. The else statement is what we've already seen and is registering the protocol handler to the OS application handler.
A line that is one you usually have to dig for, is* *==process.defaultApp== which is basically a flag that determines how many positional arguments are already a part of process.argv because electron passes certain environment variables in certain situations.
So, we've registered our protocol, what next
we can now trigger our application launch from external applications but how does our application actually respond to these 'external stimuli'. Well, we have to register handlers for these events. This code is platform specific (==because of course it is==) because platforms like Windows and Linux have certain conditions about multiple application instances
in windows/linux instances, your code will usually look something like this
const instanceLock=app.requestSingleInstanceLock();
if(!instanceLock){
app.quit();
}else{
app.on("second-instance",(event,command,workingDirectory)=>{
if(mainWindow){
if(mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
const url=command.pop();
// do something with url
}
});
app.whenReady().then(()=>{
createWindow();
});
}
Now, we'll take this line by line because there's a lot going on here. for one, the introduction of
app.requestSingleInstanceLock();
requestSingleInstanceLock, checks whether the running instance of your application is the 'primary' instance and will proceed as planned otherwise the process should quit immediately since it's parameters and resources will be redirected to another instance.
From lines 5 through 16, we are basically proceeding as though the instance lock has been acquired, meaning the current instance of the application is the primary instance. In this, we listen for the "second-instance" event of the 'app' structure which provides us and event, command and workingDirectory. What we will actually be using is 'command', because you can basically consider this process the same as running
code .
in your terminal to open vscode in your current working directory. As such, to obtain our intent from the command, we'd in most cases be doing
command.pop()
this gives our our protocol url with whatever we have appended to the end of it, awaiting parsing and usage.
However, apps targetting the MacOS platform have a slightly more straightforward time. The code is actually annoyingly more simple but whatever.
app.on("open-url",(event,url)=>{
// do something with url
})
So essentially, your application would have two entrypoints for deep linking into the application if you are targetting multiple platforms but, by the power of code, the url handling can be abstracted away and the code shared.
If you've reached this point, I can say you've succesfully implemented deep linking into your application and are ready to go. However, there are somethings you need to take into consideration when packaging your application.
Your packager, whether electron-forge or electron-builder (I'm biased towards electron builder), has to be aware of your protocol otherwise this effort would be a wasted one
// electron-builder
"build": {
"productName": "MyApp",
"appId": "com.my.app",
"protocols": {
"name": "my-app-protocol",
"schemes": [
"my-app"
]
},
"win": {...},
"linux": {...},
"mac": {...}
}
// electron-forge
{
"config": {
"forge": {
"packagerConfig": {
"protocols": [
{
"name": "Electron Fiddle",
"schemes": ["electron-fiddle"]
}
]
},
"makers": [
{
"name": "@electron-forge/maker-deb",
"config": {
"mimeType": ["x-scheme-handler/electron-fiddle"]
}
}
]
}
}
}
The only difference with electron-packager is protocols is part of the packager options and not the packager config
const packager = require('@electron/packager')
packager({
// ...other options...
protocols: [
{
name: 'Electron Fiddle',
schemes: ['electron-fiddle']
}
]
}).then(paths => console.log(`SUCCESS: Created ${paths.join(', ')}`))
.catch(err => console.error(`ERROR: ${err.message}`))
At this point, you can rub your hands together, give yourself a pat on the back and "stand proud", you've implemented deep linking into your electron application.
I do hope you found this article usefult as I decided to go into depth explaining some of the things that are just listed off within the documentation and pulling them together in a more coherrent way.