@wrnexus/mobile
SSR-safe compatibility access to Capacitor plugins.
Install the package
After WorkRoot approves private registry access, install the release-aligned package:
bun add @wrnexus/mobile@0.8.7Request preview access. Never put registry tokens in source control.
SSR-safe access to Capacitor plugins from WRNexusJS browser code.
Overview
@wrnexus/mobile keeps optional native imports out of server rendering while giving browser-owned modules one consistent registry for Capacitor plugins. During SSR, mobile.isNative() is false and mobile.platform() is "web".
Install a plugin through the WRNexusJS CLI so the web and native projects stay aligned:
wrnexus mobile add @capacitor/camera @capacitor/haptics
Usage
Register and invoke a Capacitor plugin
Import Capacitor packages only from browser-owned code, never from API routes or SSR helpers.
import { Camera, CameraResultType } from "@capacitor/camera";
import { mobile } from "@wrnexus/mobile";
mobile.registerPlugin("Camera", Camera);
export async function takePhoto() {
if (!mobile.isNative()) return null;
return mobile.invoke("Camera", "getPhoto", {
quality: 85,
resultType: CameraResultType.Uri,
});
}
Provide a browser fallback
whenNative runs the first callback only in a Capacitor WebView and can return a web/SSR-safe fallback everywhere else.
import { Haptics, ImpactStyle } from "@capacitor/haptics";
import { mobile } from "@wrnexus/mobile";
mobile.registerPlugin("Haptics", Haptics);
export const confirmAction = () =>
mobile.whenNative(
() => mobile.invoke("Haptics", "impact", { style: ImpactStyle.Medium }),
() => navigator.vibrate?.(30),
);
Read an optional plugin without throwing
import type { NetworkPlugin } from "@capacitor/network";
import { mobile } from "@wrnexus/mobile";
const network = mobile.plugin<NetworkPlugin>("Network");
const status = network ? await network.getStatus() : { connected: true, connectionType: "unknown" };
API
registerPlugin(name, instance)registers a browser-imported plugin.plugin(name)returns a plugin orundefined;requirePlugin(name)throws when absent.invoke(plugin, method, options?)calls a registered method and returns its result.whenNative(native, fallback?)selects native behavior without breaking SSR.isNative()andplatform()report the current Capacitor environment.
Unavailable required plugins throw MobileUnavailableError with an actionable message.
The package also provides portable application-facing primitives:
listenDeepLinksnormalizes initial and live links with an allowed-scheme list.PushNotificationsperforms permission gating and validates registrations.SecureStoragenamespaces and validates keys over an application-supplied encryptedOfflineQueuepersists bounded sync batches through a pluggable durable store.
Keychain/Keystore adapter; it does not mislabel browser localStorage as secure.
Requirements / Notes
- Capacitor plugin imports must remain in browser-owned modules.
@wrnexus/mobilere-exportsnativefrom@wrnexus/nativefor applications that
prefer the higher-level cross-platform capability API.
Complete TypeScript API
Generated from the exact installed package declarations.
export { native } from '@wrnexus/native';
interface DeepLink {
url: URL;
path: string;
query: URLSearchParams;
}
declare function parseDeepLink(value: string, schemes?: string[]): DeepLink | null;
interface DeepLinkSource {
current?(): Promise<string | undefined>;
subscribe(listener: (url: string) => void): void | (() => void);
}
/** Normalize initial and live native links and ignore malformed/unapproved schemes. */
declare function listenDeepLinks(source: DeepLinkSource, listener: (link: DeepLink) => void, schemes?: string[]): () => void;
interface PushRegistration {
token: string;
platform?: string;
}
interface PushAdapter {
permission(): Promise<"granted" | "denied" | "prompt" | "unavailable">;
requestPermission?(): Promise<"granted" | "denied">;
register(): Promise<PushRegistration>;
subscribe?(listener: (notification: unknown) => void): () => void;
}
declare class PushNotifications {
private readonly adapter;
constructor(adapter: PushAdapter);
register(): Promise<PushRegistration>;
subscribe(listener: (notification: unknown) => void): () => void;
}
interface SecureStorageAdapter {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
remove(key: string): Promise<void>;
}
declare class SecureStorage {
#private;
private readonly adapter;
private readonly namespace;
constructor(adapter: SecureStorageAdapter, namespace?: string);
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<void>;
remove(key: string): Promise<void>;
}
interface OfflineTask<T = unknown> {
id: string;
type: string;
payload: T;
createdAt: number;
attempts: number;
}
interface OfflineTaskStore {
load(): Promise<OfflineTask[]>;
save(tasks: OfflineTask[]): Promise<void>;
}
declare function memoryOfflineTaskStore(): OfflineTaskStore;
declare class OfflineQueue {
#private;
private readonly store;
constructor(store?: OfflineTaskStore);
process<T>(type: string, handler: (payload: T) => Promise<void>): void;
add<T>(type: string, payload: T): Promise<OfflineTask<T>>;
sync(limit?: number): Promise<{
completed: number;
failed: number;
}>;
size(): Promise<number>;
}
interface MobileEnvironment {
platform: string;
native: boolean;
online: boolean;
userAgent?: string;
}
declare function mobileEnvironment(): MobileEnvironment;
/** @wrnexus/mobile — SSR-safe access to Capacitor's native bridge. */
type MobilePlatform = "ios" | "android" | "web" | string;
interface CapacitorBridge {
isNativePlatform?: () => boolean;
getPlatform?: () => MobilePlatform;
Plugins?: Record<string, unknown>;
}
declare class MobileUnavailableError extends Error {
constructor(message?: string);
}
/** Register a plugin imported by browser-only application code. */
declare function registerPlugin<T extends object>(name: string, instance: T): T;
/** True only inside a native Capacitor iOS or Android WebView. SSR-safe. */
declare function isNative(): boolean;
/** Current Capacitor platform, falling back to `web` during SSR and in browsers. */
declare function platform(): MobilePlatform;
/** Return an injected Capacitor plugin, or undefined when it is unavailable. */
declare function plugin<T extends object>(name: string): T | undefined;
/** Require an installed native plugin and produce a useful error when absent. */
declare function requirePlugin<T extends object>(name: string): T;
/** Invoke a plugin method without importing native code into an SSR module. */
declare function invoke<TResult = unknown>(pluginName: string, method: string, options?: unknown): Promise<TResult>;
/** Run native behavior when available, with an optional SSR/web fallback. */
declare function whenNative<T>(native: () => T | Promise<T>, fallback?: () => T | Promise<T>): Promise<T | undefined>;
declare const mobile: {
isNative: typeof isNative;
platform: typeof platform;
registerPlugin: typeof registerPlugin;
plugin: typeof plugin;
requirePlugin: typeof requirePlugin;
invoke: typeof invoke;
whenNative: typeof whenNative;
};
export { type CapacitorBridge, type DeepLink, type DeepLinkSource, type MobileEnvironment, type MobilePlatform, MobileUnavailableError, OfflineQueue, type OfflineTask, type OfflineTaskStore, type PushAdapter, PushNotifications, type PushRegistration, SecureStorage, type SecureStorageAdapter, invoke, isNative, listenDeepLinks, memoryOfflineTaskStore, mobile, mobileEnvironment, parseDeepLink, platform, plugin, registerPlugin, requirePlugin, whenNative };
Examples
Copy-ready examples from the installed package documentation.
Register and invoke a Capacitor plugin
import { Camera, CameraResultType } from "@capacitor/camera";
import { mobile } from "@wrnexus/mobile";
mobile.registerPlugin("Camera", Camera);
export async function takePhoto() {
if (!mobile.isNative()) return null;
return mobile.invoke("Camera", "getPhoto", {
quality: 85,
resultType: CameraResultType.Uri,
});
}Provide a browser fallback
import { Haptics, ImpactStyle } from "@capacitor/haptics";
import { mobile } from "@wrnexus/mobile";
mobile.registerPlugin("Haptics", Haptics);
export const confirmAction = () =>
mobile.whenNative(
() => mobile.invoke("Haptics", "impact", { style: ImpactStyle.Medium }),
() => navigator.vibrate?.(30),
);Read an optional plugin without throwing
import type { NetworkPlugin } from "@capacitor/network";
import { mobile } from "@wrnexus/mobile";
const network = mobile.plugin<NetworkPlugin>("Network");
const status = network ? await network.getStatus() : { connected: true, connectionType: "unknown" };