npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@appolabs/appo

v2.0.2

Published

JavaScript bridge SDK for native app features in React Native WebViews

Readme

@appolabs/appo

JavaScript bridge SDK for accessing native device features from web apps running inside React Native WebViews.

Installation

npm install @appolabs/appo
pnpm add @appolabs/appo
yarn add @appolabs/appo

Or include via script tag (auto-initializes window.appo):

<script src="https://unpkg.com/@appolabs/appo"></script>

Quick Start

import { getAppo } from '@appolabs/appo';

const appo = getAppo();

if (appo.isNative) {
  const status = await appo.push.requestPermission();
  if (status === 'granted') {
    const token = await appo.push.getToken();
  }
}

Initialization

The SDK provides two initialization functions and a singleton pattern:

import { getAppo, initAppo } from '@appolabs/appo';

// Option 1: getAppo() - initializes on first call, returns existing instance after
const appo = getAppo();

// Option 2: initAppo() - explicit initialization, attaches to window.appo
const appo = initAppo();

When loaded via <script> tag, the SDK auto-initializes and attaches to window.appo. Subsequent calls to getAppo() or initAppo() return the same singleton instance.

The isNative property indicates whether the SDK is running inside a native Appo container (true) or a regular browser (false).

const appo = getAppo();
console.log(appo.isNative);  // true inside Appo app, false in browser
console.log(appo.version);   // SDK version string

API Reference

Push Notifications

interface PushApi {
  requestPermission(): Promise<PermissionStatus>;
  getToken(): Promise<string | null>;
  onMessage(callback: (message: PushMessage) => void): () => void;
  onResponse(callback: (response: PushResponse) => void): () => void;
}

Request permission and retrieve the push token:

const status = await appo.push.requestPermission();
// status: 'granted' | 'denied' | 'undetermined'

if (status === 'granted') {
  const token = await appo.push.getToken();
  // token: string | null
}

Subscribe to incoming push notifications:

const unsubscribe = appo.push.onMessage((message) => {
  console.log(message.title, message.body, message.data);
});

// Later: stop listening
unsubscribe();

Subscribe to notification tap events (when user taps a notification):

const unsubscribe = appo.push.onResponse((response) => {
  console.log(response.title, response.body, response.actionIdentifier);
});

Biometrics

interface BiometricsApi {
  isAvailable(): Promise<boolean>;
  authenticate(reason: string): Promise<boolean>;
}
const available = await appo.biometrics.isAvailable();
if (available) {
  const success = await appo.biometrics.authenticate('Confirm your identity');
}

Camera

interface CameraApi {
  requestPermission(): Promise<PermissionStatus>;
  takePicture(): Promise<CameraResult>;
}
const status = await appo.camera.requestPermission();
if (status === 'granted') {
  const result = await appo.camera.takePicture();
  // result: { uri: string, base64?: string, width: number, height: number }
}

Location

interface LocationApi {
  requestPermission(): Promise<PermissionStatus>;
  getCurrentPosition(): Promise<Position>;
}
const status = await appo.location.requestPermission();
if (status === 'granted') {
  const position = await appo.location.getCurrentPosition();
  // position: { latitude, longitude, altitude?, accuracy?, timestamp }
}

Haptics

interface HapticsApi {
  impact(style: HapticImpactStyle): void;
  notification(type: HapticNotificationType): void;
}
appo.haptics.impact('light');        // 'light' | 'medium' | 'heavy'
appo.haptics.notification('success'); // 'success' | 'warning' | 'error'

Storage

interface StorageApi {
  get(key: string): Promise<string | null>;
  set(key: string, value: string): Promise<void>;
  delete(key: string): Promise<void>;
}
await appo.storage.set('auth_token', 'abc123');
const token = await appo.storage.get('auth_token');
await appo.storage.delete('auth_token');

Share

interface ShareApi {
  open(options: ShareOptions): Promise<ShareResult>;
}
const result = await appo.share.open({
  title: 'Check this out',
  message: 'Content to share',
  url: 'https://example.com',
});
// result: { success: boolean, action?: string }

Network

interface NetworkApi {
  getStatus(): Promise<NetworkStatus>;
  onChange(callback: (status: NetworkStatus) => void): () => void;
}
const status = await appo.network.getStatus();
// status: { isConnected: boolean, type: 'wifi' | 'cellular' | 'none' | 'unknown' }

const unsubscribe = appo.network.onChange((status) => {
  console.log('Network changed:', status.isConnected, status.type);
});

Device

interface DeviceApi {
  getInfo(): Promise<DeviceInfo>;
}
const info = await appo.device.getInfo();
// info: { platform, osVersion, appVersion, deviceId, deviceName, isTablet }

Error Handling

All bridge operations that require a native environment throw AppoError with categorized error codes:

import { AppoError, AppoErrorCode } from '@appolabs/appo';

try {
  const token = await appo.push.getToken();
} catch (error) {
  if (error instanceof AppoError) {
    switch (error.code) {
      case AppoErrorCode.NOT_NATIVE:
        // Not running inside a native Appo app
        break;
      case AppoErrorCode.TIMEOUT:
        // Native layer did not respond within 30s
        break;
      case AppoErrorCode.NATIVE_ERROR:
        // Native handler returned an error
        break;
      case AppoErrorCode.BRIDGE_UNAVAILABLE:
        // Bridge communication channel unavailable
        break;
    }
    console.log(error.message, error.detail);
  }
}

AppoError extends Error, so existing catch blocks continue to work without modification.

class AppoError extends Error {
  readonly code: AppoErrorCode;
  readonly detail?: string;
}

enum AppoErrorCode {
  NOT_NATIVE = 'NOT_NATIVE',
  TIMEOUT = 'TIMEOUT',
  NATIVE_ERROR = 'NATIVE_ERROR',
  BRIDGE_UNAVAILABLE = 'BRIDGE_UNAVAILABLE',
}

Logging

The SDK produces no console output by default. Use setLogger to observe bridge activity:

import { setLogger } from '@appolabs/appo';

setLogger((level, message, data) => {
  // level: 'debug' | 'warn' | 'error'
  console.log(`[appo:${level}]`, message, data);
});

// Disable logging
setLogger(null);

Log events include message sends, responses received, timeouts, and parse failures. Payloads are excluded from log data to prevent leaking sensitive information.

Browser Fallbacks

All APIs provide fallback behavior when running outside a native Appo container:

| Feature | Method | Fallback | |---------|--------|----------| | Push | requestPermission() | Returns 'denied' | | Push | getToken() | Returns null | | Push | onMessage() | Returns no-op unsubscribe | | Push | onResponse() | Returns no-op unsubscribe | | Biometrics | isAvailable() | Returns false | | Biometrics | authenticate() | Returns false | | Camera | requestPermission() | Returns 'denied' | | Camera | takePicture() | Throws Error | | Location | requestPermission() | Returns 'denied' | | Location | getCurrentPosition() | Throws Error | | Haptics | impact() | No-op | | Haptics | notification() | No-op | | Storage | get() / set() / delete() | Uses localStorage | | Share | open() | Uses navigator.share if available, otherwise { success: false } | | Network | getStatus() | Returns { isConnected: navigator.onLine, type: 'unknown' } | | Network | onChange() | Listens to browser online/offline events | | Device | getInfo() | Returns user agent-based info with osVersion: 'web' |

TypeScript

All types are exported for use in consuming applications:

import type {
  // Core
  Appo,
  PermissionStatus,

  // Push
  PushApi,
  PushMessage,
  PushResponse,

  // Camera
  CameraApi,
  CameraResult,

  // Location
  LocationApi,
  Position,

  // Haptics
  HapticsApi,
  HapticImpactStyle,
  HapticNotificationType,

  // Storage
  StorageApi,

  // Share
  ShareApi,
  ShareOptions,
  ShareResult,

  // Network
  NetworkApi,
  NetworkStatus,

  // Device
  DeviceApi,
  DeviceInfo,

  // Biometrics
  BiometricsApi,

  // Bridge internals
  BridgeResponse,
  BridgeEvent,

  // Logging
  AppoLogLevel,
  AppoLogger,
} from '@appolabs/appo';

// Value exports
import {
  getAppo,
  initAppo,
  setLogger,
  AppoError,
  AppoErrorCode,
  isBridgeResponse,
  isBridgeEvent,
  VERSION,
} from '@appolabs/appo';

Architecture

The SDK communicates with the native React Native layer through window.ReactNativeWebView.postMessage():

Web App (SDK)                          React Native App
─────────────────                      ─────────────────
sendMessage(type, payload)
  │
  ├─ Generate unique ID
  ├─ Register pending callback
  ├─ postMessage({ id, type, payload })
  │                                    onMessage(event)
  │                                      ├─ Parse message
  │                                      ├─ Dispatch to handler
  │                                      └─ Send response ──────┐
  │                                                              │
  ◄──────────────────── { id, success, data } ──────────────────┘
  │
  ├─ Match response to pending request by ID
  ├─ Resolve/reject promise
  └─ Return data to caller

Event broadcasts flow from native to web:

React Native App                       Web App (SDK)
─────────────────                      ─────────────────
Native event fires
  │
  ├─ broadcastEvent({ event, data })
  │                                    handleNativeMessage()
  │                                      ├─ Detect event (no id field)
  │                                      └─ Notify registered listeners
  │
  └─ Example events:
     'push.message'    → push.onMessage() callbacks
     'push.response'   → push.onResponse() callbacks
     'network.change'  → network.onChange() callbacks

All sendMessage calls have a default 30-second timeout. Message IDs use the format msg_{timestamp}_{counter} for correlation.

License

MIT