Build the Ionic Native wrapper for a Cordova plugin

April 18, 2022

In this post we will learn how to create the @awesome-cordova-plugins wrapper to make using Cordova plugins easier. This simplification is achieved by mapping Cordova plugin’s callback funtions to Promises and Observables.

@awesome-cordova-plugins was previously known as @ionic-native.
The project only changed its name, the functionality continues the same.

This post is the continuation of the following Cordova plugin tutorial:

So we will build the wrapper for the plugin of that tutorial.

You could find the source code of this tutorial on GitHub:
https://github.com/adrian-bueno/multiplatform-cordova-plugin-example (at packages > awesome-cordova-plugins-example).

We will start by defining how to approach this depending if you are building a public or private plugin. Afterwards we will see how to write the wrapper using TypeScript.

My plugin will be public

If your plugin will be public, just fork the official @awesome-cordova-plugins repository. Then clone your fork:

git clone https://github.com/<your-username>/awesome-cordova-plugins.git

Once its finished cloning, create a new directory with the name of your plugin inside src/@awesome-cordova-plugins/plugins.

Then create a new file index.ts. We will fulfill this file in the section: Pluggin mapper code.

When the code is ready, create a pull request to the official repository. After your pull request is acepted, your plugin wrapper will be available to download in npm with the package name @awesome-cordova-plugins/<your-plugin-directory-name>..

My plugin will be private

To build a private plugin wrapper we have 2 options:

  1. Create a fork of the official repository. Delete all the plugins from src/@awesome-cordova-plugins/plugins and just create directories for all our plugin wrappers. Then upload the generated packages to our private npm repository.

  2. (This is just a personal preference) Fork the official repository and adapt it to only contain one plugin wrapper. And delete all the files we don’t need.

Either way, we will have to keep the build scripts updated with the ones in the official repository.

I will show you how I adapted the official repository to only contain one plugin wrapper and reduce the repository code.

You could skip all this guide and just use my code on GitHub.

First, clone the official repository and change its name:

git clone --depth=1 https://github.com/danielsogl/awesome-cordova-plugins.git awesome-cordova-plugins-example

Delete the following directories and files:

πŸ“ .git
πŸ“ .github
πŸ“ .husky
πŸ“ docs
πŸ“ scripts
 β”œβ”€β”€ πŸ“ docs
 β”œβ”€β”€ πŸ“ tasks
 β”‚    └── πŸ“„ publish.ts
 β”œβ”€β”€ πŸ“ templates
 └── πŸ“„ logger.ts **
πŸ“ src
 └── πŸ“ @awesome-cordova-plugins
πŸ“„ .editorconfig **
πŸ“„ .eslintignore **
πŸ“„ .eslintrc **
πŸ“„ .gitbook.yml
πŸ“„ .prettierignore **
πŸ“„ CHANGELOG.md **
πŸ“„ CODE_OF_CONDUCT.md
πŸ“„ DEVELOPER.md
πŸ“„ gulpfile.js
πŸ“„ prettier.config.js **
πŸ“„ LICENSE **
πŸ“„ renovate.json
πŸ“„ tsconfig.core.json

You could keep the files marked with **, but adapt them to your code and preferences. If you remove logger.ts you would have to use console.log in the files that use this logger.

Delete de .git directory and create a new repository.

Remove the paths property from tsconfig.json and edit the value from the include property:

{
  "compilerOptions": {
    "baseUrl": ".",
    "declaration": true,
    "stripInternal": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "noImplicitAny": true,
    "module": "es2015",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "src",
    "target": "es5",
    "skipLibCheck": true,
    "lib": ["es2017", "dom"],
    "inlineSources": true,
    "inlineSourceMap": true
  },
  "include": ["src/**/*.ts"], <---
  "angularCompilerOptions": {
    "genDir": "aot"
  }
}

Remove the paths from scripts/tsconfig.json too:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "lib": ["es6"]
  },
  "exclude": ["node_modules"]
}

Clean the package.json and install @awesome-cordova-plugins/core as a dev dependency. Also change the name and version to your wrappers name:

{
  "name": "awesome-cordova-plugins-example",
  "version": "1.0.0",
  "description": "Native plugin wrapper for cordova-plugin-example",
  "scripts": {
    "build:esm": "ts-node -P scripts/tsconfig.json scripts/tasks/build-esm",
    "build:es5": "ts-node -P scripts/tsconfig.json scripts/tasks/build-es5",
    "build:ngx": "ts-node -P scripts/tsconfig.json scripts/tasks/build-ngx",
    "build": "npm run build:esm && npm run build:ngx && npm run build:es5",
    "prebuild": "rimraf -rf dist"
  },
  "peerDependencies": {
    "@awesome-cordova-plugins/core": "^5.1.0",
    "rxjs": "^5.5.0 || ^6.5.0"
  },
  "devDependencies": {
    "@angular/common": "11.2.14",
    "@angular/compiler": "11.2.14",
    "@angular/compiler-cli": "11.2.14",
    "@angular/core": "11.2.14",
    "@awesome-cordova-plugins/core": "^5.41.0",
    "@types/cordova": "0.0.34",
    "@types/fs-extra": "9.0.13",
    "@types/lodash": "4.14.181",
    "@types/node": "16.11.26",
    "@types/rimraf": "3.0.2",
    "@types/webpack": "5.28.0",
    "fs-extra": "10.0.1",
    "lodash": "4.17.21",
    "rimraf": "3.0.2",
    "rollup": "2.70.1",
    "rxjs": "6.6.7",
    "terser-webpack-plugin": "5.3.1",
    "ts-node": "10.7.0",
    "typescript": "4.1.6",
    "unminified-webpack-plugin": "3.0.0",
    "webpack": "5.71.0",
    "winston": "3.7.2",
    "zone.js": "0.11.5"
  }
}

Now we have to replace in all the TypeScript files from the scripts directory:

We also have to change the variable PLUGIN_PATHS from scrits/build/helpers.ts:

export const PLUGIN_PATHS = [join(ROOT, 'src', 'index.ts')];

And remove transpileNgxCore() from scripts/tasks/build-ngx.ts, since we have delete the core module from src.

Finally, create a new TypeScript script inside scripts/tasks called build-packagejson.ts:

import { readFileSync, writeFileSync } from 'fs-extra';
import path = require('path');
import { ROOT } from '../build/helpers';

const packageJson = JSON.parse(readFileSync(path.join(ROOT, "package.json")).toString());

const newPackageJson = {
    name: packageJson.name,
    version: packageJson.version,
    module: packageJson.module,
    main: packageJson.main,
    peerDependencies: packageJson.peerDependencies
}

writeFileSync(path.join(ROOT, "dist/package.json"), JSON.stringify(newPackageJson, null, 4));

This scripts creates a new package.json inside the dist directory. It copies the package.json from the root and ‘deletes’ all the not needed properties for the compiled wrapper. Add the properties you want to newPackageJson.

Update the build script from package.json:

{
  "build:packagejson": "ts-node -P scripts/tsconfig.json scripts/tasks/build-packagejson",
  "build": "npm run build:esm && npm run build:ngx && npm run build:es5 && npm run build:packagejson",
}

Before running this scripts, we have to add our wrapper code in src/index.ts.

Plugin mapper code

index.ts (Open on GitHub)

import { Injectable } from '@angular/core';
import { Cordova, Plugin, AwesomeCordovaNativePlugin } from '@awesome-cordova-plugins/core';
import { Observable } from 'rxjs';

// We will define first two interfaces for
// the Bitcoin current price response.
// I don't know at this moment if we
// can define this interfaces in different files.
// I wasn't completely successful when I tried to
// do it, at least with the 'build-ngx' script.

export interface BPI {
  code: string;
  description: string;
  rate: string;
  rate_float: number;
  symbol: string;
}

export interface BitcoinCurrentPriceResponse {
  bpi: {
    EUR: BPI;
    GBP: BPI;
    USD: BPI;
  },
  chartName: string;
  disclaimer: string;
  time: {
    updated: string;
    updatedISO: string;
    updateduk: string;
  }
}


// We need two decorators @Plugin and @Injectable.
//
// For the @Plugin properties, remember what we wrote
// in our plugin's plugin.xml file:
//
// <!-- www -->
// <js-module name="CordovaPluginExample" src="www/CordovaPluginExample.js">
//   <clobbers target="cordova.plugins.CordovaPluginExample" />
// </js-module>
//
// pluginName == js-module.name
// pluginRef == clobbers.target

@Plugin({
  pluginName: 'CordovaPluginExample',
  plugin: 'cordova-plugin-example',
  pluginRef: 'cordova.plugins.CordovaPluginExample',
  repo: 'https://github.com/adrian-bueno/multiplatform-cordova-plugin-example',
  platforms: ['Android', 'iOS', 'Electron']
})
@Injectable()
export class AwesomeCordovaPluginExample extends AwesomeCordovaNativePlugin {

  // For every method, use the @Cordova decorator.
  // Since in our Cordova plugin JavaScript file (www/CordovaPluginExample.js)
  // we defined callback functions before parameters,
  // now we have to declare 'callbackOrder' as 'reverse'.

  @Cordova({
    callbackOrder: 'reverse'
  })
  greeting(name?: string): Promise<string> { return null; }

  // If a method can return multiple values over time,
  // declare the property 'observable' as 'true'.

  @Cordova({
    observable: true,
    callbackOrder: 'reverse'
  })
  countdownTimer(seconds?: number): Observable<number> { return null; }

  // We don't have to add any logic to the methods, just return null.

  @Cordova({
    callbackOrder: 'reverse'
  })
  writeFile(fileName: string, text: string): Promise<string> { return null; }

  @Cordova({
    callbackOrder: 'reverse'
  })
  bitcoinCurrentPrice(): Promise<BitcoinCurrentPriceResponse> { return null; }

}

Check more @Plugin and @Cordova options (and other details) in the official developer documentation.

Test the wrapper in a Cordova app

This wrapper works with:

Read the official documentarion for more details and code examples.

I’m going to use an Angular (+ Cordova) app to test the wrapper.

# Install Angular CLI
npm i -g @angular/cli

# Create an Angular app
ng new <app-name>
cd <app-name>

Change Angular output path to www and add ./ as the baseHref in angular.json:

{
...

"architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "baseHref": "./",  <---
            "outputPath": "www",  <---
            "index": "src/index.html",
            "main": "src/main.ts",
...
}

Copy the config.xml file we created in the main tutorial. You can copy it from my GitHub repository.

Now we can install the Cordova CLI and add the platforms we need:

# Install Cordova CLI
npm i -D cordova

# Create the www directory just
# to make Cordova not complaint
# while installing platforms
mkdir www

# Add Android platform
npx cordova platform add android

# Add iOS platform (only if you are using macOS)
npx cordova platform add ios

# Add Electron platform
npx cordova platform add electron@3.0.0
# Or if you want to use my cordova-electron fork
# to have the "keepCallback" functionality
npx cordova platform add https://github.com/adrian-bueno/cordova-electron#feature/keep-callback

Add www, plugins and platforms to the .gitignore file.

Install @awesome-cordova-plugins/core:

npm i @awesome-cordova-plugins/core

Install the wrapper:

npm i <path-to-wrapper>/awesome-cordova-plugins-example/dist

Install the plugin:

npx cordova plugin add <path-to-plugin>/cordova-plugin-example

Tip: Move all the dependencies to devDependencies (package.json) if you are building an Electron app. This will keep the output app smaller. Angular depencies are only needed in the compilation phase, but not while the app is running.

Add the cordova.js script to index.html:

<script src="cordova.js"></script>

Delete the default code from app.component.html and app.component.ts.

Add the wrapper class to the providers list in app.module.ts:

// In Angular we must import the /ngx path
import { AwesomeCordovaPluginExample } from 'awesome-cordova-plugins-example/ngx';

@NgModule({
  // ...
  
  providers: [AwesomeCordovaPluginExample],
  
  // ...
})
export class AppModule { }

If your plugin is public, its package name would be @awesome-cordova-plugins/example instead of awesome-cordova-plugins-example.

Now we are goint to create some buttons in AppComponent, like we did in the main plugin tutorial.

We will start with app.component.ts:

import { Component } from '@angular/core';
import { AwesomeCordovaPluginExample } from 'awesome-cordova-plugins-example/ngx'

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  constructor(private awesomeCordovaPluginExample: AwesomeCordovaPluginExample) {}

  greeting(name?: string) {
    this.awesomeCordovaPluginExample.greeting(name)
      .then(res => console.log(res))
      .catch(error => console.error(error));
  }

  countdown(seconds: number) {
    this.awesomeCordovaPluginExample.countdownTimer(seconds)
      .subscribe(sec => console.log(sec));
  }

  writeFile(fileName: string, text: string) {
    this.awesomeCordovaPluginExample.writeFile(fileName, text)
      .then(res => console.log(res))
      .catch(error => console.error(error));
  }

  bitcoinCurrentPrice() {
    this.awesomeCordovaPluginExample.bitcoinCurrentPrice()
      .then(res => console.log(res))
      .catch(error => console.error(error));
  }

}

app.component.html:

<button (click)="greeting('World!')">
  Greeting "Hello World!"
</button>

<button (click)="greeting()">
  Greeting "Hello!"
</button>

<hr>

<button (click)="countdown(12)">
  Countdown (12)
</button>

<hr>

<button (click)="writeFile('cordova-plugin-example.txt', 'Hello there πŸ‘‹')">
  Write file
</button>

<hr>

<button (click)="bitcoinCurrentPrice()">
  Bitcoin current price
</button>

Our test app is ready. Now we are going to compile the Angular app and run it in a Cordova platform.

npm run build && npx cordova run android

npm run build && npx cordova run ios

npm run build && npx cordova run electron

Note: You could not run cordova-electron@3.0.0 with Node.js 16, use version 14.

You should see the plugin results in the console after clicking a button. Read more details on how you could see Android and iOS app logs in the main post.