Skip to main content

Implementing a Capacitor Plugin

Capacitor plugins provide a practical approach to structured communication through a Portal. The Capacitor bridge is used under the hood in Portals, allowing Capacitor plugins to be used.

In this step, you will author a Capacitor plugin to log analytics.

note

Ensure you are serving the Expenses web app using the Portals CLI before proceeding.

Exploring the problem

Business sponsors of the Expenses web app would like to introduce analytics, with the following requirements:

  1. The ability to log navigation to a new screen shall exist.
  2. The ability to log specific actions taken in the app shall exist.
  3. Every analytic entry shall track the platform the log occurred on.
  4. Analytics shall be logged when the web app is accessed through mobile or on the web.

Based on the requirements, the same actions must be available whether the Expenses web app is presented through a Portal or accessed on a web browser, but the implementation of the actions differ based on platform.

Authoring a Capacitor plugin is ideal in this case. The functionality of a Capacitor plugin is specified by a TypeScript API. Android, iOS, and web developers create platform-specific implementations that adhere to the defined API. During runtime, a Capacitor plugin dynamically directs calls to the appropriate implementation.

info

Capacitor plugins perform platform-detection under the hood, making them a good abstraction for processes that require different implementations on different platforms.

Defining the API contract

Based on the requirements above, the following interface is reasonable for the analytics plugin:


_10
interface AnalyticsPlugin {
_10
logAction(opts: { action: string, params?: any }): Promise<void>;
_10
logScreen(opts: { screen: string, params?: any }): Promise<void>;
_10
}

Notice that the interface doesn't address the requirement of tracking the running platform. This is an implementation detail that can be addressed when platform-specific code is written.

Create a new file, web/shared/portals/analytics-plugin.ts and populate the file with the interface above.

Registering the plugin

The Capacitor plugin API is available as part of the @capacitor/core npm package. For the purpose of this training, it will be added as a dependency of the web package containing the project's shared Portals code:

terminal

_10
cd ./web/shared/portals
_10
pnpm add @capacitor/core

Use @capacitor/core to register the plugin with the Expenses web app:

web/shared/portals/analytics-plugin.ts

_10
import { registerPlugin } from "@capacitor/core";
_10
_10
interface AnalyticsPlugin {
_10
logAction(opts: { action: string; params?: any }): Promise<void>;
_10
logScreen(opts: { screen: string; params?: any }): Promise<void>;
_10
}
_10
_10
export const Analytics = registerPlugin<AnalyticsPlugin>(
_10
"Analytics"
_10
);

Register the analytics plugin using the registerPlugin() method.

The string "Analytics" sets the plugin name, and it must be consistent across different platform implementations.

web/shared/portals/analytics-plugin.ts
web/shared/portals/index.ts

_14
/**
_14
* COMPLETE: See "Stubbing Initial Context for Development"
_14
*/
_14
import { resolveInitialContext } from "./initial-context";
_14
export { resolveInitialContext };
_14
_14
/**
_14
* COMPLETE: See "Publishing Messages with PubSub"
_14
*/
_14
import { publishNavigateBackMessage } from "./pub-sub";
_14
export { publishNavigateBackMessage };
_14
_14
import { Analytics } from "./analytics-plugin";
_14
export { Analytics };

Replace the existing implementation in web/shared/portals/index.ts and point to the new implementation.

Register the analytics plugin using the registerPlugin() method.

The string "Analytics" sets the plugin name, and it must be consistent across different platform implementations.

Replace the existing implementation in web/shared/portals/index.ts and point to the new implementation.

web/shared/portals/analytics-plugin.ts

_10
import { registerPlugin } from "@capacitor/core";
_10
_10
interface AnalyticsPlugin {
_10
logAction(opts: { action: string; params?: any }): Promise<void>;
_10
logScreen(opts: { screen: string; params?: any }): Promise<void>;
_10
}
_10
_10
export const Analytics = registerPlugin<AnalyticsPlugin>(
_10
"Analytics"
_10
);

Save the code, then tap either the plus icon or an individual expense's edit (pencil) icon. If you were to monitor network traffic (optional), you would notice requests sent to an analytics endpoint. These requests contain data about the event, including a property platform indicating the running platform. For this demo, the analytics plugin has been implemented in the native binary running on your device or simulator.

Everything works when the plugin runs on a mobile platform, but if you navigate to http://localhost:5173 you will encounter the following error:


_10
"Analytics" plugin is not implemented on web

As a web developer, your responsibility is to author the web implementation of a Capacitor plugin. In the following section, you will write the web implementation of the analytics plugin to meet the requirements outlined at the beginning of this step.

Web implementation

The Expenses web app needs to record analytic events whether it is being presented through a Portal, or running as a standalone application on the web.

To that end, the analytics Capacitor plugin must contain an implementation when it is used on the web. You can write the web implementation for a Capacitor plugin by writing a class that extends WebPlugin and providing an instance of the class as part of registerPlugin():

web/shared/portals/analytics-plugin.ts

_18
import { WebPlugin, registerPlugin } from "@capacitor/core";
_18
_18
interface AnalyticsPlugin {
_18
logAction(opts: { action: string; params?: any }): Promise<void>;
_18
logScreen(opts: { screen: string; params?: any }): Promise<void>;
_18
}
_18
_18
class AnalyticsWeb extends WebPlugin implements AnalyticsPlugin {
_18
async logAction(opts: { action: string; params?: any }): Promise<void> {
_18
throw new Error("Method not implemented.");
_18
}
_18
_18
async logScreen(opts: { screen: string; params?: any }): Promise<void> {
_18
throw new Error("Method not implemented.");
_18
}
_18
}
_18
_18
export const Analytics = registerPlugin<AnalyticsPlugin>("Analytics");

Add a class that extends WebPlugin and implements AnalyticsPlugin in web/shared/portals/analytics-plugin.ts.

web/shared/portals/analytics-plugin.ts

_21
import { WebPlugin, registerPlugin } from "@capacitor/core";
_21
_21
interface AnalyticsPlugin {
_21
logAction(opts: { action: string; params?: any }): Promise<void>;
_21
logScreen(opts: { screen: string; params?: any }): Promise<void>;
_21
}
_21
_21
class AnalyticsWeb extends WebPlugin implements AnalyticsPlugin {
_21
async logAction(opts: { action: string; params?: any }): Promise<void> {
_21
throw new Error("Method not implemented.");
_21
}
_21
_21
async logScreen(opts: { screen: string; params?: any }): Promise<void> {
_21
throw new Error("Method not implemented.");
_21
}
_21
}
_21
_21
export const Analytics = registerPlugin<AnalyticsPlugin>(
_21
"Analytics",
_21
{ web: new AnalyticsWeb() }
_21
);

Register an instance of this class for the web implementation as part of the registerPlugin() call.

web/shared/portals/analytics-plugin.ts

_24
import { WebPlugin, registerPlugin } from "@capacitor/core";
_24
import { httpClient } from "@jobsync/api";
_24
_24
interface AnalyticsPlugin {
_24
logAction(opts: { action: string; params?: any }): Promise<void>;
_24
logScreen(opts: { screen: string; params?: any }): Promise<void>;
_24
}
_24
_24
class AnalyticsWeb extends WebPlugin implements AnalyticsPlugin {
_24
async logAction(opts: { action: string; params?: any }): Promise<void> {
_24
const { action, params } = opts;
_24
await httpClient.post("/analytics", { action, params, platform: "web" });
_24
}
_24
_24
async logScreen(opts: { screen: string; params?: any }): Promise<void> {
_24
const { screen, params } = opts;
_24
await httpClient.post("/analytics", { screen, params, platform: "web" });
_24
}
_24
}
_24
_24
export const Analytics = registerPlugin<AnalyticsPlugin>(
_24
"Analytics",
_24
{ web: new AnalyticsWeb() }
_24
);

Use utility methods available as part of the local @jobsync/api package to make calls to the analytics backend.

Note that platform: 'web' is being added to the event payload.

Add a class that extends WebPlugin and implements AnalyticsPlugin in web/shared/portals/analytics-plugin.ts.

Register an instance of this class for the web implementation as part of the registerPlugin() call.

Use utility methods available as part of the local @jobsync/api package to make calls to the analytics backend.

Note that platform: 'web' is being added to the event payload.

web/shared/portals/analytics-plugin.ts

_18
import { WebPlugin, registerPlugin } from "@capacitor/core";
_18
_18
interface AnalyticsPlugin {
_18
logAction(opts: { action: string; params?: any }): Promise<void>;
_18
logScreen(opts: { screen: string; params?: any }): Promise<void>;
_18
}
_18
_18
class AnalyticsWeb extends WebPlugin implements AnalyticsPlugin {
_18
async logAction(opts: { action: string; params?: any }): Promise<void> {
_18
throw new Error("Method not implemented.");
_18
}
_18
_18
async logScreen(opts: { screen: string; params?: any }): Promise<void> {
_18
throw new Error("Method not implemented.");
_18
}
_18
}
_18
_18
export const Analytics = registerPlugin<AnalyticsPlugin>("Analytics");

Return to the browser and notice that no more errors remain. If you monitor network traffic (optional), you will notice network requests made to an analytics endpoint with a data payload containing platform: 'web', confirming that the web implementation is in use. The analytics plugin determined that the Expenses web app is running on a web platform, and picked the appropriate plugin implementation to use.

info

Detailed information about Capacitor plugins and the Capacitor Plugin API can be found here.

Conclusion

With the completion of the analytics Capacitor plugin, the Expenses web app is ready to be bundled within the Jobsync superapp! In this training module, you exercised the various ways web apps can communicate through a Portal. Furthermore, you used the Portals CLI to set up a development workflow to test and debug a web app running within the Portal the mobile application will present it in.

You now have the tools in place to take any web app and make it Portals-ready. Happy coding!! 🤓