Custom url scheme


#1

Hi,
New to electron.js. Want to create a custom url scheme myapp like this myapp://request/path?param1=... which when called in a BROWSER opens my app and processes the response. Is it possible to achieve.

Should i be looking at protocol module? Does it only registers custom url schemes within the application scope?


#2

Note: running OS X 10.10.4
Here is the code from main.js that registers the protocol.
Note: don’t mind the bad code layout

var app = require('app');
var BrowserWindow = require('browser-window');
var path = require('path');

// Report crashes to our server.
require('crash-reporter').start();

var mainWindow = null;

// Quit when all windows are closed.
app.on('window-all-closed', function() {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform != 'darwin') {
    app.quit();
  }
});

var url_scheme = 'testt';

app.on('ready', function() {
  var protocol = require('protocol');   
  protocol.registerProtocol(url_scheme, function(request) {
    var url = request.url.substr(7);
    console.log('Url', url);

    protocol.registerStandardSchemes([url_scheme]);

    return new protocol.RequestStringJob({
      data: 'Herro world!'
    });
    // return new protocol.RequestFileJob(path.normalize(__dirname + '/' + url));
  }, function (error, scheme) {
    console.log('Sheme', scheme);
    if (!error)
      console.log(scheme, ' registered successfully')

    protocol.isHandledProtocol(url_scheme, function(canHandle) {
      console.log('Can handle custom url scheme:', ~~canHandle);
    });

    createWindow();
  });

  
});

var createWindow = function() {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 800, height: 600});

  // and load the index.html of the app.
  mainWindow.loadUrl('file://' + __dirname + '/index.html');

  // Open the devtools.
  mainWindow.openDevTools();

  // Emitted when the window is closed.
  mainWindow.on('closed', function() {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null;
  });
}

Console output when running app.

electron .
# Sheme testt
# testt  registered successfully
# Can handle custom url scheme: 1

When going to testt://foo/bar on Safari it indicates there isn’t an application that registered the protocol.

Found a command that lists all registered schemes. But my custom url scheme doesn’t seem to be registered. What am I doing wrong?

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump|egrep "(bindings.*\:$)"|sort

#3

I think protocol only lets you intercept network data that is emanating from one of your app’s Renderer windows.


#4

Any updates on this? I would also really like to implement this in an application :slightly_smiling:


#5

Well I implemented custom protocol succesfully in OS X.

In the main.js which is main file of my electron application (main entry within package.json). I listen to the url such as below, where UrlBuffer is my written singleton class that is shared beetween renderer and main.

Not that to add the protocol I use electron-packager which builds the executables. To add the protocol to the build I add the following config entries for electron-packager.

Note: You will need to build a application using electron-packager once and launch the app. It will add the custom url sheme to OS X. After that you can do developing with the now registered custom url schema.


#6

Ahh thanks for this :slightly_smiling: I seem to have it kinda working…

If the app is already running, I can click a protocol url in a web-browser and open the app at the desired page.

If the app isn’t running, I can open it, although the open-url event either seems to be not firing or is firing too early. I’m slightly unclear about the order in which events get triggered if the app is started by a user clicking on a protocol url?

Does app need to be ready before I can bind open-url


#7

I’ ve the same problem at OSX!
Did you solved in any way?

My question:

Another Q/A that seems to solve the issue, but didn’t manage to make it play for my situation

I am looking to use electron-builder package only since it plays well at Windows and at OSX with this exception only.
API is there, but without working example i am i am stuck.


#8

@MerlinMason
In case you haven’t found a solution yet, here’s a short explanation of what is happening.

Quick answer: No, you don’t need to wait until the app is ready to bind the listener, but you will have to wait until the BrowserWindow is ready before you try to send the IPC-message.

The app object is an extension of EventEmitter from Nodejs. This can use listeners like on and once and so forth. Each of these listeners will trigger once for each event (the once-version removing itself from the listeners after exactly 1 event).

Then lets say we trigger the open-url event through clicking a link while the electron application is not running. Because of the asynchronous nature of event-listeners the BrowserWindow object you try to pass the IPC-message to might not be ready to accept such a message.

In other words there is a race condition akin to this:
Which happens first:
1: the open-url and forwarding an event through the IPC, or
2: the BrowserWindow is instantiated and ready to receive and handle an IPC-message

What we ended up doing is having an asynchronous function checking if the BrowserWindow in question is ready using Promise, and setInterval.

/**
 * Searches the BrowserWindow-array for a window with the supplied title.
 * It returns a promise and retries a couple of times (default 5) until
 * either resolving with an object reference to a BrowserWindow or an
 * error that should be handled accordingly.
 *
 * The main use for this method is when you're not sure the window has been
 * fully instantiated. E.g. when getting an 'open-url' event when the
 * application is closed. The event fires before the mainWindow is properly
 * bootstrapped and shown, and we have to wait until ready to pass it on
 * to the loaded React-app.
 */
const getBrowserWindowByTitle = async (title, maxRetries = 5) => {
  let retries = 0;

  return new Promise((resolve, reject) => {
    const intervalRef = setInterval(() => {
      retries += 1;
      const window = BrowserWindow.getAllWindows().find(win => win.getTitle() === title);
      if (window) {
        clearInterval(intervalRef);
        electronLogger.info(`Found a BrowserWindow object with title: ${title}`);
        resolve(window);
      } else if (retries > maxRetries) {
        clearInterval(intervalRef);
        const errorMessage = `Couldn't find a BrowserWindow titled "${title}" after ${maxRetries} retries.`;
        reject(new Error(errorMessage));
      } else {
        electronLogger.info(`No BrowserWindow object titled ${title} found yet. Retrying ${maxRetries - retries} more times.`);
      }
    }, 1000);
  });
};

// Then we do this for the open-url event
app.on('open-url', (event, url) => {
  event.preventDefault();
  getBrowserWindowByTitle(mainWindowTitle)
    .then(browserWindow => sendProtocolEvent(browserWindow, url))
    .catch(e => electronLogger.error(e));
});

We opted to attempt to find the window by searching for its title, but one can just as well use a map where you store the reference when the mainwindow like below and just poll the map instead of the BrowserWindow-array. The drawback (we think) is that it’s harder to find dynamically added windows.

const windowReferences = {};

// Then when you create your window store it on your predefined key:
windowReferences["predefinedKey"] = createMainWindow();