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

react-native-nitro-call-detector

v1.0.6

Published

React Native Nitro Module for detecting phone calls on iOS and Android — cellular (PSTN) by default, with opt-in VoIP detection (CallKit / ConnectionService) for WhatsApp, Zoom, Teams, Google Meet and friends. Zero manifest declarations by default. Built

Readme

react-native-nitro-call-detector

React Native Nitro Module for detecting phone calls on iOS and Android — cellular (PSTN) by default, with opt-in VoIP detection (CallKit / ConnectionService) for WhatsApp, Zoom, Teams, Google Meet and friends. Zero manifest declarations by default. Built for meeting-notes apps, compliance recorders, and anything else that needs to know when the user is on a call.


[!NOTE]

  • This library was originally created for my work app, where we needed to prompt "want our app to join and take notes?" no matter which one they picked up.
  • We looked at rolling our own CXCallObserver bridge and an InCallService and quickly ran out of afternoon. Every existing library on npm either only did PSTN, only did iOS, or wrapped a deprecated API that shipped before ConnectionService self-managed calls existed. Nothing gave us PSTN + optional VoIP in one API, on both platforms, without polluting your Play Store submission.
  • A calling app is a scary thing to ship by accident. The moment your AndroidManifest.xml declares an InCallService, Google Play treats you as a calling app — the same category as full-blown dialers — even if all you want is "tell me when a call starts." Every third-party library that ships a manifest with telephony declarations forces that category onto every host app that installs it. This library ships an empty Android manifest. You opt in by adding a few lines to your own app manifest — and you only add the VoIP piece if you actually want it.
  • A missed call event means a missed meeting. If the rep gets on the call before we notice, our app joins 30 seconds late, misses the opening context, and the summary is worse. Call detection is the trigger for the whole recording / joining / transcription pipeline downstream, so it has to be fast, synchronous, and never miss a state change.

What you get out of the box:

  • PSTN detection on both platforms with zero manifest surgery on iOS and one permission on Android (READ_PHONE_STATE, non-restricted, no Play Console approval needed)
  • state transitions: incoming → dialing → connected → disconnected, exactly one event per transition
  • Synchronous isInCall() and getActiveCalls() — safe to read from render
  • addCallListener(cb) returns an unsubscribe closure — no removeListener string keys, no leaks
  • Uses TelephonyCallback on Android (API 31+) with a PhoneStateListener fallback on older devices — modern APIs, not deprecated ones
  • No InCallService, no calling-app intent filter, no restricted permissions

Opt-in extras (see docs/android-setup.md):

  • VoIP detection on Android — WhatsApp, Zoom, Teams, Google Meet, Telegram, Signal — by declaring an InCallService in your app manifest. Ships the service implementation in the AAR; it stays dormant until you declare it.
  • VoIP detection on iOS — always on. CallKit exposes cellular and VoIP through the same observer with no way to separate them.

What this library does NOT do (by design):

  • No meeting URL extraction. iOS sandboxing and Android app isolation forbid reading data out of Zoom/Teams/Meet. For scheduled meetings, pair with a calendar integration; for ad-hoc meetings, prompt the user to paste.
  • No runtime permission requester. Use PermissionsAndroid from React Native, or react-native-permissions, or your own module. Then call CallDetector.refreshPermissions(). One less activity in your manifest, one less thing Google reviewers see.
  • No call recording. For long-form microphone capture that survives interruptions, use react-native-nitro-audio-anvil. This library is the trigger; Anvil is the recorder.
  • No detection of non-CallKit / non-ConnectionService VoIP. A handful of older or regional apps route audio without registering with the OS call framework. They are invisible to any observer-based approach. Platform limitation, not a bug.

If your app needs to know the second a call starts, on both platforms, without lighting up Google Play's calling-app review — this is the detector.

🎥 Demo


📦 Installation

yarn add react-native-nitro-call-detector react-native-nitro-modules
cd ios && pod install

Then follow Android setup to add the manifest declaration for the detection level you want (PSTN or PSTN + VoIP). Nothing to add on iOS.

[!IMPORTANT]

  • iOS: Uses CXCallObserver (CallKit). Zero manifest / permission setup. Detects every CallKit-integrated app — cellular, FaceTime, WhatsApp, Zoom, Teams, Google Meet, Telegram, Signal — in one stream.
  • Android: Uses TelephonyCallback (API 31+) or PhoneStateListener (older). PSTN only by default. VoIP detection is opt-in via manifest — see android-setup.md.
  • Tested on React Native 0.85+ with the New Architecture (required by Nitro Modules).

🧠 Overview

| Feature | Implementation | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | iOS backend | CXCallObserver on the main queue | | Android backend (PSTN) | TelephonyCallback (API 31+) / PhoneStateListener (older) | | Android backend (VoIP) | InCallService, opt-in via host app manifest | | PSTN detection | ✅ Both platforms, no manifest changes on iOS | | VoIP detection | ✅ iOS (bundled with PSTN via CallKit) · ✅ Android with manifest opt-in | | Source app identification | Android VoIP mode only — package name of the hosting app | | State transitions | incomingdialingconnecteddisconnected, one event each | | Sync queries | isInCall(), getActiveCalls() — safe from render | | Event stream | addCallListener(cb) → unsubscribe, JS-thread delivery | | Runtime permissions | Handled by the host app (PermissionsAndroid etc). Library provides refreshPermissions() to reactivate the backend after a grant. | | Library manifest | Empty. Nothing is added to your app unless you declare it yourself. | | Threading | One owner thread per platform, no locks, no JS-thread blocking |


📚 Documentation

| Doc | When to read | | -------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Android setup | Read first. Manifest snippets for PSTN-only and PSTN+VoIP, plus Play Store readiness notes. | | iOS quirks | Before shipping on iOS — CallKit's opaque observer, why type is unknown, background behaviour | | OEM quirks | Before shipping on Android — the "Calling accounts" toggle Xiaomi/Vivo/Oppo/Realme hide | | Troubleshooting | When something breaks — symptom-first debugging |


⚙️ Basic Usage

import { PermissionsAndroid, Platform } from 'react-native';
import { CallDetector, type CallInfo } from 'react-native-nitro-call-detector';

// One-shot check
if (CallDetector.isInCall()) {
  const [call] = CallDetector.getActiveCalls();
  console.log(call);
}

// Live stream
const unsubscribe = CallDetector.addCallListener((call) => {
  switch (call.state) {
    case 'incoming':
      // Prompt: "Our App detected an incoming call — take notes?"
      break;
    case 'connected':
      // Start local backup recording, dispatch the bot
      break;
    case 'disconnected':
      // Wrap up, upload, transcribe
      break;
  }
});

// later
unsubscribe();

Requesting the runtime permission on Android

The library does NOT request permissions itself. Use your app's existing flow:

async function enableCallDetection() {
  if (Platform.OS !== 'android') return;

  const granted = await PermissionsAndroid.request(
    PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE
  );

  if (granted === PermissionsAndroid.RESULTS.GRANTED) {
    // Kick the native backend so it starts observing right now.
    CallDetector.refreshPermissions();
  }
}

Routing on source app (Android VoIP mode only)

CallDetector.addCallListener((call) => {
  if (call.state !== 'connected') return;

  switch (call.sourceApp) {
    case 'us.zoom.videomeetings':
      return startZoomFlow();
    case 'com.microsoft.teams':
      return startTeamsFlow();
    case 'net.whatsapp.WhatsApp':
      return startWhatsAppFlow();
    default:
      return startGenericFlow(call);
  }
});

sourceApp is undefined on iOS and undefined on Android in PSTN-only mode. If you need this signal, enable VoIP mode via android-setup.md.

React hook

import { useEffect, useState } from 'react';
import { CallDetector, type CallInfo } from 'react-native-nitro-call-detector';

export function useActiveCall(): CallInfo | null {
  const [call, setCall] = useState<CallInfo | null>(null);

  useEffect(() => {
    setCall(CallDetector.getActiveCalls()[0] ?? null);
    return CallDetector.addCallListener((next) => {
      setCall(next.state === 'disconnected' ? null : next);
    });
  }, []);

  return call;
}

🔐 Permissions

iOS — Info.plist

Nothing to add. CXCallObserver requires no permissions and no Info.plist keys. Import and use.

Android

Handled by the host app. See android-setup.md for the exact snippet. Short version:

  • PSTN only: declare READ_PHONE_STATE in your manifest, request it at runtime via PermissionsAndroid, call CallDetector.refreshPermissions() after the grant.
  • PSTN + VoIP: also declare the CallDetectorInCallService component. See the doc.

READ_CALL_LOG is not declared. It's a Play Store restricted permission that only unlocks the remote phone number for PSTN. If you need it, declare and request it yourself in the host app.


📡 API

CallDetector.isInCall(): boolean
CallDetector.getActiveCalls(): CallInfo[]
CallDetector.hasRequiredPermissions(): boolean
CallDetector.refreshPermissions(): boolean  // Android; no-op on iOS
CallDetector.addCallListener(cb: (call: CallInfo) => void): () => void

CallInfo

interface CallInfo {
  readonly id: string;
  readonly type: 'pstn' | 'voip' | 'unknown';
  readonly state: 'incoming' | 'dialing' | 'connected' | 'disconnected';
  readonly remoteHandle?: string; // phone number or SIP URI, best-effort
  readonly sourceApp?: string; // Android VoIP mode only, e.g. "us.zoom.videomeetings"
  readonly startedAt: number; // ms since epoch
  readonly connectedAt?: number; // ms since epoch, set when state → connected
}

Field availability

| Field | iOS | Android PSTN mode | Android VoIP mode (opt-in) | | -------------- | -------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------- | | id | ✅ CXCall.uuid | ✅ generated | ✅ telecom Call hashCode | | type | ⚠️ almost always unknown | ✅ always pstn | ✅ pstn or voip, inferred from sourceApp | | state | ✅ | ✅ | ✅ | | remoteHandle | ❌ CallKit hides it | ⚠️ with READ_PHONE_STATE on Android <12; needs READ_CALL_LOG on Android 12+ | ✅ for VoIP; ⚠️ same restriction as PSTN mode | | sourceApp | ❌ CallKit hides it | ❌ not observed in PSTN mode | ✅ package name of the hosting app |


🎯 What's actually detected

| App | iOS | Android PSTN mode | Android VoIP mode (opt-in) | | --------------- | --- | ----------------- | ----------------------------------- | | Cellular / PSTN | ✅ | ✅ | ✅ | | FaceTime | ✅ | — | — | | WhatsApp | ✅ | ❌ | ✅ | | Zoom | ✅ | ❌ | ✅ (self-managed ConnectionService) | | Microsoft Teams | ✅ | ❌ | ✅ | | Google Meet | ✅ | ❌ | ✅ | | Telegram | ✅ | ❌ | ✅ | | Signal | ✅ | ❌ | ✅ |


🧩 Supported Platforms

| Platform | Status | | -------------------- | -------------------------------------------------------------------------------------------------- | | iOS | ✅ Fully Supported | | Android | ✅ Fully Supported (PSTN default, VoIP opt-in) | | iOS Simulator | ⚠️ Partial — CXCallObserver does not fire for cellular. Test on a real device. | | Android Emulator | ⚠️ Partial — cellular via emulator console works; VoIP apps are hit-and-miss. Test on real device. |


🔧 Building the library

yarn install
yarn nitrogen        # generates nitrogen/generated from src/specs/*.nitro.ts
yarn typecheck

nitrogen/generated/ must be committed and shipped in the npm package.


🤝 Contributing

Contributions are welcome!


🪪 License

MIT © Gautham Vijayan


Made with ❤️ and Nitro Modules