@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-nativeInstall required peer dependencies:
npm install @react-native-async-storage/async-storage \
@react-native-firebase/app \
@react-native-firebase/messaging \
@notifee/react-nativeSetup
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, addsPOST_NOTIFICATIONSpermission - iOS: writes
GoogleService-Info.plist, adds it to Xcode resources, callsFirebaseApp.configure()in AppDelegate, setsUIBackgroundModesandaps-environment
Then run prebuild:
npx expo prebuildReact 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_KEYThe 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
CirclePushPermissionErrorif 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
