Build a Cordova plugin for Electron

April 18, 2022
Last update: August 1, 2024
Table of contents

Main post

This post forms part of this tutorial:

You might want to check that post first before continuing with this one. You will learn how to:

You could find the source code of this tutorial on GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example.

Start

This tutorial is for cordova-electron 4.0.0

We are going to implement in Node.js the 4 methods defined in our JavaScript interface www/CordovaPluginExample.js:

Create a new directory electron inside src.

Then create the files CordovaPluginExample.js and package.json inside electron.

πŸ“ cordova-plugin-example
└── πŸ“ src
    └── πŸ“ electron
        β”œβ”€β”€ πŸ“„ CordovaPluginExample.js
        └── πŸ“„ package.json

Then add the minimum configuration to this new package.json:

{
  "name": "cordova-plugin-example",
  "version": "0.0.0",
  "main": "CordovaPluginExample.js",
  "cordova": {
    "serviceName": "CordovaPluginExample"
  }
}

Electron runs with at least 2 processes: the main process and the renderer process. (We can spawn more process if needed). The main process runs in a Node.js environment. A redenderer process is spawned for every opened BrowserWindow and is in charge of rendering the web content. Basically, main process == Node.js, renderer process == Chromium. More info

The code in src/electron runs in the main process.

External dependencies

We are going to use axios (a promise based HTTP client for the browser and Node.js) to obtain Bitcoin’s current price from https://api.coindesk.com/v1/bpi/currentprice.json .

To use external dependencies just add them to src/electron/package.json:

{
  "name": "cordova-plugin-example",
  "version": "0.0.0",
  "main": "CordovaPluginExample.js",
  "cordova": {
    "serviceName": "CordovaPluginExample"
  },
  "dependencies": {
    "axios": "^0.26.0"
  }
}

plugin.xml configuration

The plugin.xml configuration for Electron is pretty simple:

<!-- Electron -->
<platform name="electron">
  <framework src="src/electron"/>
</platform>

That’s it. We don’t have to especify permissions or edit any other file, like in Android and iOS.

Electron source code structure

πŸ“ cordova-plugin-example
β”œβ”€β”€ πŸ“ src
β”‚   β”œβ”€β”€ πŸ“ android
β”‚   β”œβ”€β”€ πŸ“ ios
β”‚   └── πŸ“ electron
β”‚       β”œβ”€β”€ πŸ“„ CordovaPluginExample.js
β”‚       └── πŸ“„ package.json
β”œβ”€β”€ πŸ“ www
β”‚   └── πŸ“„ CordovaPluginExample.js
β”œβ”€β”€ πŸ“„ .gitignore
β”œβ”€β”€ πŸ“„ package.json
β”œβ”€β”€ πŸ“„ plugin.xml
└── πŸ“„ README.md

Node.js source code

CordovaPluginExample.js

Open on GitHub

const fs = require("fs");
const path = require("path");
const os = require("os");
const axios = require("axios").default;


// This is our simplest example.
// Returns the string "Hello {name}!"
// or "Hello!" if a name is not received.
function greeting(args) {
  const [name] = args;
  return name ? `Hello ${name}!` : "Hello!";
}

// Returns a number every second, from "seconds" parameter value to 0.
// NOT POSSIBLE TO IMPLEMENT IT WITH cordova-electron 4.0.0+.
// cordova-electron 4.0.0+ doesn't support the keepCallback
// functionality that is available in Android and iOS.
function countdownTimer(args) {
  // const [seconds] = args;
  throw "NOT_IMPLEMENTED";
}

// Writes a file in the user's Documents folder.
function writeFile(args) {
  const [fileName, text] = args;

  if (!fileName || !text) {
    throw "BAD_ARGS";
  }

  try {
    const dir = path.join(os.homedir(), "Documents");
    fs.mkdirSync(dir, { recursive: true });
    const filePath = path.join(dir, fileName);
    fs.writeFileSync(filePath, text);
  } catch (error) {
    console.error(error);
    throw "WRITE_ERROR"
  }
}

// This example is just to show how can we use
// the external dependencies we declared previously,
// in this case, axios.
// 
// Functions could also be async and return Promises.
async function bitcoinCurrentPrice() {
  try {
    const response = await axios.get("https://api.coindesk.com/v1/bpi/currentprice.json")
    return response.data;
  } catch (error) {
    console.error(error);
    throw "REQUEST_ERROR";
  }
}

// Export the funtions, use the same names
// declared in www/CordovaPluginExample.js
// (in the 'action' parameter, the 4th one)
//
// exports.writeFile = function (successCallback, errorCallback, fileName, text) {
//   exec(successCallback, errorCallback, PLUGIN_NAME, 'writeFile', [fileName, text]);
// };
module.exports = {
  greeting,
  countdownTimer,
  writeFile,
  bitcoinCurrentPrice,
};

Functions can return values directly or inside a Promise.

Return multiple values (‘keepCallback’ functionality)

This part of the tutorial doesn’t use oficial cordova-electron code (at least for now).

I recently needed to build a plugin for Android and Electron in which I needed to detect when a USB device was plugged/unplugged. So I needed the keepCallback functionality. Since this functionality is not available yet in the official cordova-electron, I created a fork you could find here (branch ‘feature/keep-callback’). I also created a pull request that is waiting for validation from the official devs. I will update this tutorial if they approve this changes or add this functionality in another way.

To use this fork, just replace the version 4.0.0 from your Cordova app’s package.json with github:adrian-bueno/cordova-electron#feature/keep-callback .

Remove first cordova-electron platform:

npx cordova platform remove electron

Then add this fork URL and branch as a platform:

npx cordova platform add https://github.com/adrian-bueno/cordova-electron#feature/keep-callback

Now you will see the following in your app’s package.json:

{
  ...

  "dependencies": {
    "cordova-electron": "github:adrian-bueno/cordova-electron#feature/keep-callback"
  }
  
  ...

}

Now we can implement the code for function countdownTimer.

For this functionality I decided to use the $ character to identify functions that could return multiple values over time. By implementing it by this way there is no need to add any breaking changes for existing plugins.

We have to do the following changes:

Edit www/CordovaPluginExample.js:

...

// Detect if the app is running in the Electron platform
const runningInElectron = navigator.userAgent.indexOf("Electron") >= 0;

// If the app is running in Electron, the action must end with a $
// For the rest of platforms we continue with the same action name
exports.countdownTimer = function (successCallback, errorCallback, seconds) {
  const action = runningInElectron ? "countdownTimer$" : "countdownTimer";
  exec(successCallback, errorCallback, PLUGIN_NAME, action, [seconds]);
};

...

Edit src/electron/CordovaPluginExample.js:

...

// Add the $ to the function name.
// This functions now have 3 parameters:
// - success callback
// - error callback
// - args
//
// The callbacks have the following interfaces:
// - success(data: any, keepCallback: boolean): void
// - error(data: any): void
function countdownTimer$(success, error, args) {
  const [seconds] = args;

  let secondsLeft = seconds > 0 ? seconds : 10;

  function startTimeout() {
    const keepCallback = secondsLeft > 0;
    success(secondsLeft, keepCallback);
    if (keepCallback) {
      secondsLeft--;
      setTimeout(startTimeout, 1000);
    }
  }

  startTimeout();
}

...

module.exports = {
  greeting,
  countdownTimer$, // <= add the $
  writeFile,
  bitcoinCurrentPrice,
};

Continue reading the main post