How to get deep linking to work in Electron, on Linux

ยท

3 min read

Background

I've been working on an Electron app. The front end's all done with React, making it very convenient for us React front end guys. We wanted to implement a feature where you could open a link elsewhere on the computer - sent via messenger or whatever - and it'd open on the computer. I'm developing this on Ubuntu so I'm implementing it there first. Turns out it wasn't super straight forwards.

In the end, the key components were this:

To get Linux to be okay with handling your custom protocol (app-name://) you need to add something to your package.json file, that will get turned into a .desktop file later.

"linux": {
      "category": "Utility",
      "target": "deb",
      "mimeTypes": [
        "x-scheme-handler/app-name"
      ],
      "desktop": {
        "exec": "app-name %U"
      }
    },

the x-scheme-handler allows the computer to read the protocol app-name:// the exec command means when the link is clicked, the app will be opened. The %U is an array that will contain the URL, the rest of your link, passed in as arguments.

At this point, the app should open, but it's unaware of the URL.

To get the app to read the arguments, you can use process.argv. In the main file (main.js or index.ts)

 if (process.platform !== "darwin") {
      console.log(
        `Start up with file: ${process.argv}`,
      );
      if (process.argv.length > 1) {
        mainWindow.webContents.send("send-share-link", {
          targetLink: process.argv[1].split("app-name://")[1],
        });
      }
    }

Where mainWindow.webContents.send is called, this is firing an event that can be listened for in the main app process. Give the event a good name (send-share-link) and put the argument in an object (targetLink:)

At this point, an event is fired but we need a listener for it.

In our project, we use ipcRenderer, which is intended to provide extra security by limiting ways that can access Electron's processes.

  listenToDeepLink: (fn: any) => {
    ipcRenderer.on("send-share-link", (e, ...args) => fn(...args));
  },

Now the event has the name 'listenToDeepLink' inside the app.

In our renderer file, we have a component called AppProvider that provides all of the event listeners. If you don't have an AppProvider component in your project, this may go in the renderer file.

const { api } = window;

...

api.listenToDeepLink((data) => {
      if (data.targetLink) {
        history.push("/" + data.targetLink);
      }
    });

This establishes what will happen when this event is fired. React-router-dom's useHistory is used to push users onto the page.

At this point, the app can be opened with a link. To handle links whilst the app is already opened:

Back in the main file:

const singleInstanceLock = app.requestSingleInstanceLock();

if (!singleInstanceLock) {
  app.quit();
} else {
  app.on("second-instance", (_, argv) => {
    console.log(`Instance lock triggered`);
    if (argv.length > 1) {
      // Only try this if there is an argv (might be redundant)
      if (process.platform == "win32" || process.platform === "linux") {
        try {
          console.log(
            `Direct link to file - SUCCESS: ${argv[1]}`,
          );
          mainWindow.webContents.send("send-share-link", {
            targetLink: argv[1].split("app-name://")[1],
          });
        } catch {
          console.log(
            `Direct link to file - FAILED: ${argv[1]}`,
          );
        }
      }
    }
  });
}

singleInstanceLock prevents a second window from opening, but you can also use it to perform actions when a second window is requested. We can actually use the `app.on("second-instance") flag, which correctly reads the argvs, to send the event we made.

ย