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

@circlehq/push-react-native

v1.4.3

Published

CircleHQ Push React Native SDK — register device push tokens, identify users, and report notification events.

Downloads

1,153

Readme

@circlehq/push-react-native

CircleHQ Push SDK for React Native. Register device tokens, identify users, display notifications, and track engagement events — all backed by Circle's infrastructure. No Firebase project setup required on your end.

Features

  • Bring your own credentials (BYOC) — upload your Firebase Service Account once on the Circle dashboard; the SDK fetches your config at build time using only your Circle API key. No Firebase project setup in your app code.
  • One-call identify — pass an email or phone number; the SDK handles permissions, token fetch, and backend registration
  • Full notification lifecycle — foreground display (via notifee), background handling, cold-start tap detection
  • Offline event queue — notification events queued when offline, flushed automatically on next launch
  • Token auto-refresh — listens for FCM token rotation and re-registers silently
  • Typed event emitter — strongly-typed events for permission changes, token refresh, and notification interactions
  • Permission polling — detects permission revocation in the background without blocking the main thread

Requirements

| Peer dependency | Version | |---|---| | react-native | >=0.71.0 | | @react-native-async-storage/async-storage | >=1.19.0 | | @react-native-firebase/app | >=20.0.0 | | @react-native-firebase/messaging | >=20.0.0 | | @notifee/react-native | >=7.0.0 (optional but strongly recommended) | | @expo/config-plugins | >=7.0.0 (Expo only) |

@react-native-firebase/* and @notifee/react-native are optional at runtime — the SDK degrades gracefully if they are absent, but push notifications will not function without them.


Installation

npm install @circlehq/push-react-native
# or
yarn add @circlehq/push-react-native

Install required peer dependencies:

npm install @react-native-async-storage/async-storage \
            @react-native-firebase/app \
            @react-native-firebase/messaging \
            @notifee/react-native

Setup

Before using the SDK, upload your Firebase Service Account JSON on the Circle dashboard (Settings → Push Notifications). Circle stores it and uses it to hand back your platform-specific config on demand — you never put Firebase credentials in your app's source code.

Expo

Add the config plugin to your app.json or app.config.ts with your Circle API key:

{
  "plugins": [
    ["@circlehq/push-react-native/plugin", {
      "circleApiKey": "YOUR_CIRCLE_API_KEY",
      "androidNotificationIcon": "./assets/notification_icon.png"
    }]
  ]
}

At expo prebuild time, the plugin fetches your config from Circle's API and:

  • Android: writes google-services.json, applies the Google Services Gradle plugin, adds POST_NOTIFICATIONS permission
  • iOS: writes GoogleService-Info.plist, adds it to Xcode resources, calls FirebaseApp.configure() in AppDelegate, sets UIBackgroundModes and aps-environment

Then run prebuild:

npx expo prebuild

React Native CLI

Run the setup script with your Circle API key from your project root — it fetches google-services.json and GoogleService-Info.plist from Circle's API and writes them into android/app/ and ios/<ProjectName>/:

npx circle-push-setup --api-key YOUR_CIRCLE_API_KEY

The script prints the remaining manual steps (Gradle plugin, AppDelegate, Xcode resource). Then follow the React Native Firebase setup guide and the notifee installation guide for the rest of the native configuration.


Background Handler (Required)

This import must be the very first line of your app entry file, before any React component or other import. It registers native background handlers synchronously at module evaluation time. If it runs too late, background push events will be silently dropped.

// index.js — FIRST LINE
import '@circlehq/push-react-native/background';

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('MyApp', () => App);

For Expo Router, add it as the first line of app/_layout.tsx:

// app/_layout.tsx — FIRST LINE
import '@circlehq/push-react-native/background';

import { Stack } from 'expo-router';
// ...

Quick Start

import CirclePush from '@circlehq/push-react-native';

// 1. Initialize (call once, e.g. in App.tsx useEffect)
await CirclePush.init({
  apiKey: 'cir_live_xxxxxxxxxxxx',
});

// 2. Identify a user — triggers permission prompt and device registration
const device = await CirclePush.identify({
  email: '[email protected]',
});

console.log(device.deviceId, device.pushToken);

API Reference

CirclePush.init(config)

Initialize the SDK. Must be called and resolved before any other method.

await CirclePush.init({
  apiKey: 'cir_live_xxxxxxxxxxxx',   // required
  debug: __DEV__,                     // verbose logging, default: false
});

Safe to call multiple times with the same config — subsequent calls are no-ops. Throws CirclePushConfigError if called with a different config after initialization.

Config options:

| Option | Type | Default | Description | |---|---|---|---| | apiKey | string | — | Circle External API key. Required. | | apiBaseUrl | string | https://api.circlehq.co | Override the Circle API base URL | | debug | boolean | false | Enable verbose console logging | | autoRequestPermission | boolean | true | Auto-prompt for permission on identify() when status is default | | androidNotificationIcon | string | 'notification_icon' | Android small icon resource name from res/drawable/ | | androidChannelId | string | 'circle-push-default' | Android notification channel ID |


CirclePush.identify(identity)

Register the current user's device. Handles the full flow: permission request → FCM token fetch → backend registration.

const device = await CirclePush.identify({
  email: '[email protected]',    // at least one of email or phone is required
  phone: '+12125551234',
  firstName: 'Jane',
  lastName: 'Doe',
});
// device: { deviceId, contactId, pushToken, platform, isValid }
  • If the user's identity changes, the previous device is unregistered before registering the new one.
  • Same-identity calls are throttled to once per 60 seconds; the cached registration is returned immediately within that window.
  • Throws CirclePushPermissionError if notifications are denied or unsupported.

CirclePush.requestPermission()

Explicitly prompt the user for notification permission. Useful when autoRequestPermission is false.

const status = await CirclePush.requestPermission();
// 'granted' | 'denied' | 'default' | 'unsupported'

CirclePush.getPermissionState()

Returns the last known permission state synchronously (no async call).

const status = CirclePush.getPermissionState();

CirclePush.getToken()

Returns the stored FCM push token, or null if not yet obtained.

const token = await CirclePush.getToken();

CirclePush.refresh()

Re-fetches the FCM token and re-registers with the backend. Useful after a long session to ensure the token is current.

await CirclePush.refresh();

Returns null if the SDK has no identity or permission is not granted.


CirclePush.unregister()

Unregisters the device from the backend, deletes the local FCM token, and clears stored state.

await CirclePush.unregister();

CirclePush.on(event, callback)

Subscribe to SDK events. Returns an unsubscribe function.

const unsub = CirclePush.on('notificationClicked', ({ messageId, link, action }) => {
  if (link) router.push(link);
});

// Later:
unsub();

Available events:

| Event | Payload | Description | |---|---|---| | notificationReceived | NotificationPayload | A push notification arrived while the app is in the foreground | | notificationClicked | NotificationClickPayload | User tapped a notification or action button | | notificationDismissed | NotificationDismissedPayload | User dismissed a notification | | permissionChange | PermissionState | Notification permission status changed | | tokenRefresh | { oldToken, newToken } | FCM token was rotated | | error | CirclePushError | A non-fatal internal error occurred |


CirclePush.setDebug(enabled)

Toggle verbose logging at runtime.

CirclePush.setDebug(true);

Event Payload Types

interface NotificationPayload {
  title?: string;
  body?: string;
  image?: string;
  data?: Record<string, string>;
  actions?: NotificationAction[];
}

interface NotificationClickPayload {
  messageId?: string;
  campaignId?: string;
  link?: string;
  action?: string; // action button id, if an action button was tapped
}

interface NotificationDismissedPayload {
  messageId?: string;
  campaignId?: string;
}

Error Handling

All errors thrown from public methods are instances of CirclePushError with a stable code for programmatic branching:

import {
  CirclePushError,
  CirclePushPermissionError,
  CirclePushApiError,
} from '@circlehq/push-react-native';

try {
  await CirclePush.identify({ email: '[email protected]' });
} catch (e) {
  if (e instanceof CirclePushPermissionError) {
    // e.code === 'permission/denied' | 'permission/unsupported'
    showPermissionPrompt();
  } else if (e instanceof CirclePushApiError) {
    // e.status is the HTTP status code
    console.error('API error', e.status, e.message);
  } else if (e instanceof CirclePushError) {
    console.error(e.code, e.message, e.retryable);
  }
}

Error classes:

| Class | Codes | |---|---| | CirclePushConfigError | config/missing_api_key, config/already_initialized | | CirclePushValidationError | validation/missing_identity, validation/invalid_email, validation/invalid_phone, validation/invalid_token | | CirclePushPermissionError | permission/denied, permission/unsupported | | CirclePushTokenError | token/fetch_failed, token/delete_failed | | CirclePushApiError | api/4xx, api/5xx, api/network, api/rate_limited | | CirclePushError | runtime/not_initialized, runtime/unknown |


Complete Example

// index.js
import '@circlehq/push-react-native/background'; // MUST BE FIRST
import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('MyApp', () => App);
// App.tsx
import { useEffect } from 'react';
import CirclePush, { CirclePushPermissionError } from '@circlehq/push-react-native';

export default function App() {
  useEffect(() => {
    async function setup() {
      await CirclePush.init({
        apiKey: 'cir_live_xxxxxxxxxxxx',
        debug: __DEV__,
      });

      try {
        await CirclePush.identify({ email: '[email protected]' });
      } catch (e) {
        if (e instanceof CirclePushPermissionError) {
          console.warn('Notifications not permitted');
        }
      }
    }

    setup();

    const unsub = CirclePush.on('notificationClicked', ({ link, messageId }) => {
      console.log('Notification tapped:', messageId, link);
    });

    return () => unsub();
  }, []);

  return <YourApp />;
}

TypeScript

The SDK ships full TypeScript declarations. All public types are exported from the main entry:

import type {
  CirclePushConfig,
  Identity,
  RegisteredDevice,
  PermissionState,
  NotificationPayload,
  NotificationClickPayload,
  NotificationDismissedPayload,
  EventMap,
  EventName,
  Platform,
} from '@circlehq/push-react-native';

License

MIT