or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cookie-management.mdhttp-client.mdindex.mdplatform-runtime.mdplugin-development.mdwebview-management.md
tile.json

plugin-development.mddocs/

Plugin Development

Base classes and utilities for developing custom Capacitor plugins with cross-platform support. The plugin development API provides a foundation for creating plugins that work seamlessly across web, iOS, and Android platforms.

Capabilities

Plugin Interface

Base interface that all Capacitor plugins must implement for event handling and lifecycle management.

/**
 * Base interface all plugins must implement
 */
interface Plugin {
  /** Add event listener and return handle for removal */
  addListener(eventName: string, listenerFunc: (...args: any[]) => any): Promise<PluginListenerHandle>;
  /** Remove all event listeners */
  removeAllListeners(): Promise<void>;
}

/**
 * Handle for managing plugin event listeners
 */
interface PluginListenerHandle {
  /** Remove this specific listener */
  remove(): Promise<void>;
}

WebPlugin Base Class

Base class for implementing web platform plugin functionality with built-in event management and utility methods.

/**
 * Base class web plugins should extend
 */
class WebPlugin implements Plugin {
  /** Add event listener with automatic management */
  addListener(eventName: string, listenerFunc: ListenerCallback): Promise<PluginListenerHandle>;
  /** Remove all listeners and clean up resources */
  removeAllListeners(): Promise<void>;
  
  /** Notify all listeners for an event (protected) */
  protected notifyListeners(eventName: string, data: any, retainUntilConsumed?: boolean): void;
  /** Check if any listeners exist for an event (protected) */
  protected hasListeners(eventName: string): boolean;
  /** Register window event listener (protected) */
  protected registerWindowListener(windowEventName: string, pluginEventName: string): void;
  /** Create unimplemented error (protected) */
  protected unimplemented(msg?: string): CapacitorException;
  /** Create unavailable error (protected) */
  protected unavailable(msg?: string): CapacitorException;
}

/**
 * Callback function type for event listeners
 */
type ListenerCallback = (err: any, ...args: any[]) => void;

Usage Examples:

import { WebPlugin } from "@capacitor/core";

// Create a web implementation
class MyPluginWeb extends WebPlugin {
  async echo(options: { value: string }): Promise<{ value: string }> {
    return { value: options.value };
  }

  async startListening(): Promise<void> {
    // Register window event listener
    this.registerWindowListener('resize', 'windowResize');
  }

  private handleResize = (event: Event) => {
    // Notify plugin listeners
    this.notifyListeners('windowResize', {
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  async methodNotAvailableOnWeb(): Promise<void> {
    throw this.unimplemented('This method is not available on web');
  }

  async methodRequiringPermission(): Promise<void> {
    if (!navigator.geolocation) {
      throw this.unavailable('Geolocation not available');
    }
    // Implementation here
  }
}

// Register the plugin
import { registerPlugin } from "@capacitor/core";

interface MyPlugin {
  echo(options: { value: string }): Promise<{ value: string }>;
  startListening(): Promise<void>;
}

const MyPlugin = registerPlugin<MyPlugin>('MyPlugin', {
  web: () => new MyPluginWeb(),
});

// Use the plugin
const result = await MyPlugin.echo({ value: 'Hello' });
console.log(result.value); // 'Hello'

// Add event listener
const listener = await MyPlugin.addListener('windowResize', (data) => {
  console.log(`Window resized: ${data.width}x${data.height}`);
});

// Remove specific listener
await listener.remove();

// Or remove all listeners
await MyPlugin.removeAllListeners();

Event Management

Advanced event management system for handling plugin events with retention and window integration.

/**
 * Notify all listeners for a specific event
 * @param eventName Name of the event to fire
 * @param data Data to send to listeners
 * @param retainUntilConsumed Keep event data until listeners are added
 */
protected notifyListeners(eventName: string, data: any, retainUntilConsumed?: boolean): void;

/**
 * Check if any listeners exist for an event
 * @param eventName Name of the event to check
 * @returns true if listeners exist, false otherwise
 */
protected hasListeners(eventName: string): boolean;

/**
 * Register a window event listener that forwards to plugin listeners
 * @param windowEventName Native window event name
 * @param pluginEventName Plugin event name to forward to
 */
protected registerWindowListener(windowEventName: string, pluginEventName: string): void;

/**
 * Window listener handle for managing native event binding
 */
interface WindowListenerHandle {
  registered: boolean;
  windowEventName: string;
  pluginEventName: string;
  handler: (event: any) => void;
}

Usage Examples:

class MyAdvancedPluginWeb extends WebPlugin {
  constructor() {
    super();
    // Register window events to forward to plugin events
    this.registerWindowListener('online', 'networkChanged');
    this.registerWindowListener('offline', 'networkChanged');
  }

  async startMonitoring(): Promise<void> {
    // Simulate retained event - will be delivered to future listeners
    this.notifyListeners('initialStatus', {
      isOnline: navigator.onLine,
      timestamp: Date.now(),
    }, true); // retainUntilConsumed = true
  }

  async sendUpdate(): Promise<void> {
    // Only send if someone is listening
    if (this.hasListeners('update')) {
      this.notifyListeners('update', {
        data: 'New update available',
        timestamp: Date.now(),
      });
    }
  }

  // Window events are automatically forwarded
  private handleNetworkChange = (event: Event) => {
    this.notifyListeners('networkChanged', {
      isOnline: event.type === 'online',
      timestamp: Date.now(),
    });
  };
}

Error Handling Utilities

Built-in utilities for creating standard plugin exceptions with appropriate error codes.

/**
 * Create an unimplemented exception for unsupported methods
 * @param msg Custom error message (default: 'not implemented')
 * @returns CapacitorException with Unimplemented code
 */
protected unimplemented(msg?: string): CapacitorException;

/**
 * Create an unavailable exception for temporarily unavailable features
 * @param msg Custom error message (default: 'not available')
 * @returns CapacitorException with Unavailable code
 */
protected unavailable(msg?: string): CapacitorException;

Usage Examples:

class MyPluginWeb extends WebPlugin {
  async nativeOnlyMethod(): Promise<void> {
    // Throw unimplemented for features not available on web
    throw this.unimplemented('This method is only available on native platforms');
  }

  async cameraMethod(): Promise<void> {
    // Check for required APIs
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
      throw this.unavailable('Camera access not available in this browser');
    }
    
    try {
      // Implementation here
      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
      // Use stream...
    } catch (error) {
      throw this.unavailable('Camera permission denied or not available');
    }
  }

  async locationMethod(): Promise<void> {
    if (!navigator.geolocation) {
      throw this.unavailable('Geolocation not supported');
    }

    // Check if HTTPS is required
    if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
      throw this.unavailable('Geolocation requires HTTPS');
    }

    // Implementation here
  }
}

Plugin Registration Patterns

Common patterns for registering plugins with platform-specific implementations.

Usage Examples:

import { registerPlugin } from "@capacitor/core";

// Pattern 1: Direct implementation objects
const SimplePlugin = registerPlugin<SimplePluginInterface>('SimplePlugin', {
  web: new SimplePluginWeb(),
  android: new SimplePluginAndroid(), // If pre-loaded
});

// Pattern 2: Lazy-loaded implementations
const LazyPlugin = registerPlugin<LazyPluginInterface>('LazyPlugin', {
  web: () => import('./lazy-plugin-web').then(m => new m.LazyPluginWeb()),
  android: () => import('./lazy-plugin-android').then(m => new m.LazyPluginAndroid()),
});

// Pattern 3: Factory functions
const FactoryPlugin = registerPlugin<FactoryPluginInterface>('FactoryPlugin', {
  web: async () => {
    const config = await loadConfiguration();
    return new FactoryPluginWeb(config);
  },
});

// Pattern 4: Conditional implementations
const ConditionalPlugin = registerPlugin<ConditionalPluginInterface>('ConditionalPlugin', {
  web: () => {
    if (supportsAdvancedFeatures()) {
      return new AdvancedPluginWeb();
    } else {
      return new BasicPluginWeb();
    }
  },
});

// Pattern 5: Native-only plugin (no web implementation)
const NativeOnlyPlugin = registerPlugin<NativeOnlyPluginInterface>('NativeOnlyPlugin');
// Will throw CapacitorException with Unimplemented code on web

Plugin Testing Patterns

Recommended patterns for testing plugin implementations.

Usage Examples:

// Test web implementation
import { MyPluginWeb } from './my-plugin-web';

describe('MyPluginWeb', () => {
  let plugin: MyPluginWeb;

  beforeEach(() => {
    plugin = new MyPluginWeb();
  });

  afterEach(async () => {
    await plugin.removeAllListeners();
  });

  it('should echo value', async () => {
    const result = await plugin.echo({ value: 'test' });
    expect(result.value).toBe('test');
  });

  it('should handle listeners', async () => {
    const callback = jest.fn();
    const listener = await plugin.addListener('testEvent', callback);
    
    // Trigger event
    plugin.notifyListeners('testEvent', { data: 'test' });
    
    expect(callback).toHaveBeenCalledWith({ data: 'test' });
    
    await listener.remove();
  });

  it('should throw unimplemented for unsupported methods', async () => {
    await expect(plugin.nativeOnlyMethod()).rejects.toThrow();
  });
});