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

@amrshbib/react-native-pushbib

v0.0.2

Published

Self-hosted push notification SDK for React Native. Pushbib-style API (login, tags, click events) with zero Firebase setup in the app — no google-services.json, no GoogleService-Info.plist.

Readme

@amrshbib/react-native-pushbib

Self-hosted push notifications for React Native — three lines of setup and zero Firebase configuration inside your app:

  • ❌ no google-services.json
  • ❌ no GoogleService-Info.plist
  • ❌ no google-services gradle plugin
  • ❌ no Firebase pods on iOS (pure APNs)
  • ❌ no AppDelegate / MainApplication changes
import Pushbib from '@amrshbib/react-native-pushbib';

Pushbib.initialize({
  baseUrl: 'https://push.your-server.com',
  appId: 'PROJECT_ID', // Dashboard → App Settings
  appKey: 'nk_xxxxxxxx', // Dashboard → App Settings → Secure Keys (kind: mobile)
});

Pushbib.Notifications.requestPermission();
Pushbib.login('user-42');
Pushbib.User.addTags({ plan: 'pro', city: 'DXB' });

Pushbib.Notifications.addEventListener('click', (e) => {
  console.log('clicked', e.notification, e.result.actionId, e.result.url);
});

How it works

  • Android — the SDK bundles firebase-messaging internally and calls FirebaseApp.initializeApp(context, options) at runtime with client options fetched from your server (GET /api/mobile/v1/config). Those options are read straight out of Firebase by your server using the service-account JSON — no google-services.json anywhere, not in the app and not in the dashboard. The backend sends data-only messages to SDK devices and the SDK renders every notification natively — which is what makes action buttons, images, foreground control and consistent click handling work.
  • iOS — no Firebase at all. The SDK registers directly with APNs, sends the raw hex device token to your server, and the server delivers over HTTP/2 APNs with the .p8 key you uploaded in the dashboard. AppDelegate callbacks and the UNUserNotificationCenter delegate are hooked by runtime swizzling (installed before didFinishLaunching, forwarding to any handler your app installs) — which is also why the iOS bridge is Objective-C: swizzling needs +load and the ObjC runtime.

Install

npm install @amrshbib/react-native-pushbib
# or from the monorepo:
npm install file:../RNPackages/@amrshbib/react-native-pushbib

cd ios && pod install

Full guide with diagrams: Docs/MOBILE-SDK.md

Autolinking does the rest. Requirements: React Native ≥ 0.71 (old + new architecture via interop), Android minSdk 24, iOS 13+.

One-time dashboard setup (per app)

  1. FCM service account (sending, Android): App Settings → upload the service-account JSON.
  2. Android client config: App Settings → "Detect from service account". The server reads the client identifiers from Firebase itself.
  3. APNs key (sending, iOS): App Settings → upload the .p8 key with Key ID / Team ID / Bundle ID.
  4. Create a mobile Secure Key (optionally locked to your bundle ids) — that's the appKey.

The only platform config you can't avoid

  • iOS: enable the Push Notifications capability (Xcode → Signing & Capabilities). That's an entitlement Apple requires per app; no SDK can inject it. Recommended: also enable Background Modes → Remote notifications.
  • Android: nothing.

API

| Call | What it does | | --- | --- | | Pushbib.initialize(opts) | Boot the SDK: fetch config, mint the push token, register the device. All other calls queue behind it. | | Pushbib.login(externalId) | Bind this install to your user id (/auth/link). Survives token rotation. | | Pushbib.logout() | Unbind (/auth/unlink). | | Pushbib.getMe() | Server-side view of the logged-in user (all subscriptions + tags). | | Pushbib.User.addTag(k, v) / addTags({...}) | Merge tags (user-level when logged in, else this install). sendTag / sendTags are aliases. | | Pushbib.User.addTag(k, [a, b]) | A tag value may be a list. | | Pushbib.User.removeTag(k) / removeTags([k]) | Delete tag keys. | | Pushbib.User.removeTag(k, [a]) | Drop items from a list, keeping the rest. | | Pushbib.User.getTags() | Local cache of tags set through this install. | | Pushbib.User.getExternalId() | Current external id (or null). | | Pushbib.User.pushSubscription.getId() | Stable subscription id (treat as a secret). | | Pushbib.User.pushSubscription.getToken() | Raw push token (FCM registration token / APNs hex). | | Pushbib.User.pushSubscription.optOut() / optIn() | Server-side unsubscribe / resubscribe. | | Pushbib.Location.setLocation(lat, long) | Report coordinates for radius targeting. Shown as "Location point" on the user record. | | Pushbib.Location.getLocation() | Last coordinates reported from this install (or null). | | Pushbib.Location.clearLocation() | Forget them — the device stops matching radius segments. | | Pushbib.Notifications.requestPermission() | OS prompt (iOS, Android 13+). Resolves granted. | | Pushbib.Notifications.hasPermission() | Current permission state. | | Pushbib.Notifications.clearAll() | Clear delivered notifications (+ badge on iOS). | | Pushbib.Notifications.setForegroundDisplayEnabled(bool) / getForegroundDisplayEnabled() | Device-local toggle: show pushes while the app is open. Default: true. | | Pushbib.Notifications.setBackgroundDisplayEnabled(bool) / getBackgroundDisplayEnabled() | Device-local toggle: show pushes while the app is backgrounded (Android only). Default: true. | | Pushbib.Channels.sync() | Re-read the app's channels from /config and apply them. Runs on start already. | | Pushbib.Channels.list() | Channels on this phone with their live importance and a blocked flag (Android only). | | Pushbib.Channels.definitions() | What the server last sent, from the local cache. | | Pushbib.Channels.openSettings(key) | Open the OS settings screen for one channel. | | Pushbib.setLogLevel('verbose') | none / error / warn / info / verbose. |

Location & radius targeting

Report where the device is and the dashboard can target it by radius — "everyone within 5 km of the store". The coordinates land on the user record as Location point (Audience → Users), and the segment builder gets a Location → is within radius of filter taking latitude, longitude and a radius in metres.

The SDK deliberately never reads GPS and never asks for the location permission: your app already owns that decision (and the App Store / Play justification for it), so it hands us a fix whenever it has one.

import Geolocation from '@react-native-community/geolocation'; // or expo-location

Geolocation.getCurrentPosition(({ coords }) => {
  Pushbib.Location.setLocation(coords.latitude, coords.longitude);
});

await Pushbib.Location.getLocation();   // { lat, long } | null
await Pushbib.Location.clearLocation(); // user turned location sharing off

The value is cached on-device and replayed on every re-registration, so it survives push-token rotation and reinstalls. Call setLocation again whenever you have a newer fix — only the latest one is kept, and there is no background tracking: a device that never reports coordinates simply never matches a radius segment.

Events

// Tap on a notification or an action button — fires in every app state,
// including cold start (buffered until your listener registers).
Pushbib.Notifications.addEventListener('click', (e) => {
  e.notification; // { title, body, url, buttons, data, ... }
  e.result.actionId; // button id, or null for a body tap
  e.result.url; // data.url if the campaign set one
});

// Every incoming push, whether it was displayed or not — for logging,
// analytics, badge math. Buffered until your listener registers.
Pushbib.Notifications.addEventListener('received', (e) => {
  console.log('push received', e.notification.title, e.notification.data);
});

// Decide whether a push shows while the app is foregrounded.
// Default (no listener / no preventDefault): it shows.
Pushbib.Notifications.addEventListener('foregroundWillDisplay', (e) => {
  if (e.notification.data.silent === '1') e.preventDefault(); // synchronous!
});

Pushbib.Notifications.addEventListener('permissionChange', (granted) => {});
Pushbib.User.pushSubscription.addEventListener('change', ({ id, token, optedIn }) => {});

data.url from a campaign is opened automatically on click via Linking (deep links included). Disable with initialize({ autoOpenLaunchUrls: false }).

Navigate to a screen on click

The click event is the navigation hook — it fires when the app is in the foreground, background, and on cold start (the tap that launched the app is buffered until your listener registers, so register it early):

// e.g. with React Navigation and a navigationRef
Pushbib.Notifications.addEventListener('click', (e) => {
  const { screen, ...params } = e.notification.data; // set in the campaign's data
  if (screen) navigationRef.current?.navigate(screen, params);
});

Send the campaign with data { "screen": "OrderDetails", "orderId": "42" } and set autoOpenLaunchUrls: false if you route everything yourself.

Per-notification display control (showForeground / showBackground)

Add these keys to the notification's data payload when sending; the SDK enforces them natively (default: both true):

| Data | Effect | | --- | --- | | "showForeground": false | Not displayed while the app is open (foregroundWillDisplay is skipped entirely) — the received listener still fires. | | "showBackground": false | Android: not displayed in background/killed — delivery is silent, received is buffered for the next app session. iOS: not enforceable client-side (Apple displays background alerts; needs a Notification Service Extension — planned). | | both false | A silent data push: nothing shows anywhere (Android), your received listener gets the payload. |

The received event always fires regardless of these flags — that's the "catch and log it" hook. On iOS it fires in the foreground only (background alert pushes never wake the app without an NSE).

Device-local display toggles (foreground / background)

showForeground / showBackground above are set per message by the sender. For a setting the user controls on their own device — "show me banners while I'm using the app" vs. "while it's closed" — use the display toggles. They're persisted natively and honored on every incoming push, even in a cold process:

await Pushbib.Notifications.setForegroundDisplayEnabled(false); // silent while open
await Pushbib.Notifications.setBackgroundDisplayEnabled(false); // silent while closed (Android)
await Pushbib.Notifications.getForegroundDisplayEnabled(); // → boolean

The device stays subscribed either way (unlike optOut()): the server keeps targeting it and the received event keeps firing — only the visible banner is suppressed. setBackgroundDisplayEnabled is Android-only: on iOS the OS renders background alerts before the app runs, so it can't be honored client-side without a Notification Service Extension.

Live Activities (iOS) & Live Updates (Android)

The lock-screen card that tracks something in progress — a delivery, a ride, a match score. Your backend drives it (POST /api/server/v1/live-activity/…); the SDK's job is the device half.

How much the SDK can do differs sharply by platform, and it's worth knowing why before you plan the work.

Android — the SDK does everything

Live updates arrive on the device's normal FCM token and the native service renders an ongoing notification, replacing it in place on every update. Nothing to set up.

// Optional: declare that an activity is live so the server can address it.
// You can skip this entirely and let your backend's /live-activity/start do it.
await Pushbib.LiveActivities.register({ activityId: 'order_12345' });

// Mirror the state in-app, or draw your own richer card from contentState.
// `displayed` / `outcome` tell you whether it actually reached the tray.
Pushbib.LiveActivities.addEventListener('update', ({ liveUpdate, displayed, outcome }) => {
  console.log(liveUpdate.action, liveUpdate.contentState); // START | UPDATE | END
  if (!displayed) console.warn('live update not shown:', outcome);
});

await Pushbib.LiveActivities.end('order_12345'); // user dismissed it locally

⚠️ POST_NOTIFICATIONS must be granted, or nothing renders. From Android 13 it is a runtime permission, and until the user grants it every live update is dropped on arrival while your server still reports it as delivered. Call Pushbib.Notifications.requestPermission() at startup. The update event then reports outcome: 'notifications-disabled' rather than failing silently.

The ongoing notification is drawn from a small set of conventional keys, because Android can't know how to render an app-defined struct:

| Shown | Read from | |---|---| | Title | alert.titlecontentState.title → app name | | Body | alert.bodycontentState.body / .message / .status | | Progress bar | contentState.progress (0-100), or .indeterminate: true |

Everything else is still available in full through the update event. Live updates post to their own pushbib_live_updates channel (default importance, no badge) so users can silence them separately from regular notifications — name it with com.amrshbib.pushbib.live_update_channel_name.

Updates are silent unless the sender includes an alert, and an out-of-order delivery is dropped natively as well as server-side — FCM makes no ordering guarantee, so two updates sent moments apart can arrive reversed.

Out-of-order protection compares sequence numbers within one numbering stream (stream_id: the server session, or direct for a session-less test send). It has to: a session numbers its updates 1, 2, 3… and restarts at 1 for every new session, while a direct test send numbers by clock (~1.78e9). Comparing across those would make one test send black-hole every later real update for that activity id. A change of stream resets the mark instead of comparing against it.

"The dashboard says delivered but I see nothing"

Every way an update can fail to reach the screen is reported as outcome on the update event (and logged under the Pushbib tag):

| outcome | Meaning | |---|---| | shown / ended | It reached the tray | | notifications-disabled | POST_NOTIFICATIONS not granted, or the user turned notifications off — by far the most common cause | | stale-sequence | Older than the last update shown for this activity in the same stream | | opted-out | The device opted out of push entirely | | notify-blocked | The OS refused the notify() call | | render-error | Building the notification threw |

adb logcat -s Pushbib:V   # the same reasons, from a terminal

iOS — you own the activity, the SDK owns the server contract

The SDK cannot start or render a Live Activity, and no library can. ActivityAttributes and its SwiftUI ActivityConfiguration are types that must be declared in your Widget Extension, and Activity<Attributes> is generic over them. There is no API by which a package could do this for you.

What the SDK does own is the server contract. Your Swift observes the tokens ActivityKit hands you and reports them; registration, retry and the HTTP call are handled:

First make the bridge visible to Swift. With the default React Native setup (static libraries), add to your app's Objective-C bridging header:

#import "PushbibLiveActivities.h"

With use_frameworks!, import react_native_pushbib in the Swift file instead.

import ActivityKit

// 1. Start the activity — your attributes, your widget
let activity = try Activity<DeliveryAttributes>.request(
  attributes: DeliveryAttributes(orderId: "12345"),
  content: .init(state: .init(status: "Preparing", eta: 25), staleDate: nil),
  pushType: .token)

// 2. Report its token. Fires again on rotation, so keep observing.
Task {
  for await tokenData in activity.pushTokenUpdates {
    PushbibLiveActivities.reportActivityToken(
      PushbibLiveActivities.hexString(from: tokenData),
      forActivity: "order_12345",
      attributesType: "DeliveryAttributes")
  }
}

// 3. iOS 17.2+ — report ONCE at launch so your backend can start activities
//    while the app isn't running (POST /live-activity/start).
Task {
  for await tokenData in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
    PushbibLiveActivities.reportPushToStartToken(
      PushbibLiveActivities.hexString(from: tokenData),
      attributesType: "DeliveryAttributes")
  }
}

Reports are queued until JS is ready, so a token observed during app launch is never lost. Add NSSupportsLiveActivities: true to your Info.plist.

You don't need to call register() on iOS — reported tokens register themselves. It's there if you'd rather pass a token in from JS.

There is no update event on iOS: ActivityKit updates the widget directly and your app process is never woken.

Reconciling after a cold start

iOS can restore activities your process knows nothing about. Ask the server what it still thinks is live, then re-report tokens or end what's stale:

const sessions = await Pushbib.LiveActivities.getSessions();
// [{ activity_id, status, activity_token_registered, last_sequence_number, … }]

Android customization (optional)

AndroidManifest.xml meta-data, all optional:

<meta-data android:name="com.amrshbib.pushbib.default_notification_icon"
           android:resource="@drawable/ic_stat_notify" />
<meta-data android:name="com.amrshbib.pushbib.default_notification_color"
           android:value="#2E7DF6" />
<meta-data android:name="com.amrshbib.pushbib.default_channel_name"
           android:value="Notifications" />

Notifications post to a high-importance pushbib_default channel unless the campaign names one — see below.

Notification channels

Channels are defined server-side (dashboard → App Settings → Channels, the third section beside Project and Team) and created on the phone by the SDK. One channel holds the Android side (importance, sound, vibration, lights, lock-screen visibility, badge, DND bypass, grouping) and the iOS side (sound, interruption level, relevance score, thread id, critical alerts). An app normally runs several at once.

initialize() fetches them from /config and creates them before the first push can land. A channel created later still works on its first send: the definition travels inside the push and the SDK creates the channel on the spot.

await Pushbib.Channels.sync();
const onDevice = await Pushbib.Channels.list();      // [{ id, name, importance, blocked, showBadge }]
const muted = onDevice.filter((c) => c.blocked);
await Pushbib.Channels.openSettings('orders');       // wire this to your own settings screen

Importance has four rungs, and sound, vibration and lights are each off, default or custom:

| Field | Values | On the phone | | --- | --- | --- | | importance | low | Silent, no banner (IMPORTANCE_LOW) | | | medium | Makes a sound (IMPORTANCE_DEFAULT) | | | high | Sound + heads-up banner (IMPORTANCE_HIGH) | | | urgent | IMPORTANCE_MAX — the same threshold as high | | sound | { mode: 'off' } | No sound | | | { mode: 'default' } | The phone's default notification tone | | | { mode: 'custom', name: 'order_ding' } | res/raw/order_ding | | vibration | { mode: 'off' } | No vibration | | | { mode: 'default' } | Vibrates, OS pattern | | | { mode: 'custom', pattern: [0, 250, 250, 250] } | That pattern, in ms | | led | { mode: 'off' } | No light | | | { mode: 'default' } | Blinks, OS colour | | | { mode: 'custom', color: '#2E7DF7' } | Blinks in that colour |

urgent and high are indistinguishable on the device — Android clamps IMPORTANCE_MAX down — so Channels.list() reports 4 for both.

Sounds ship inside the app — sound mode custom names a file, it never downloads one:

| Platform | Where the file goes | What sound.name holds | | --- | --- | --- | | Android | android/app/src/main/res/raw/order_ding.mp3 | order_ding — no extension, [a-z0-9_] | | iOS | the app bundle, order_ding.caf | order_ding.caf — with extension, ≤ 30 s |

A name that is not bundled falls back to the default tone and logs a warning, so a missing file never means a silent push.

Android channels are immutable after creation, so editing the sound or importance server-side creates nf_<key>_v<n+1> and deletes nf_<key>_v<n> on the next sync — a user who muted the old one starts hearing the new one. If your app already creates its own channels, set App-owned channel id on the channel and the SDK sends that id through without creating anything — but note that a pinned id never changes, so a channel that carries one keeps whatever sound, importance and vibration the phone created it with, no matter what the dashboard says.

Known limitations (v1)

  • iOS action buttons & rich images need a Notification Service Extension (an Apple constraint, not ours). Not included yet; Android supports both natively.
  • getTags() returns the local cache; use getMe() for the server truth.
  • Badges on Android are channel-driven (setBadgeCount is iOS-only).
  • Channels.list() and openSettings(key) are Android-only; on iOS they return [] and open the app's own settings page, because iOS has no channels.
  • iOS time-sensitive and critical need an Apple entitlement — without it APNs accepts the push and iOS ignores the level.
  • iOS Live Activities need app-side Swift — the ActivityAttributes and the widget must live in your Widget Extension, so no library can supply them. The SDK provides the token bridge; see the section above. Android needs nothing.
  • Android live updates can't be scheduled for removal. An END with a future dismissal_date leaves a dismissible card rather than auto-clearing later — Android has no equivalent of ActivityKit's dismissal date.

Troubleshooting

  • pushbib/no-android-config — the server has no Android push identity for this app yet: open App Settings and run "Detect from service account".
  • pushbib/token-timeout on iOS — simulators can't get APNs tokens; use a real device and check the Push Notifications capability.
  • No pushes on iOS — upload the APNs .p8 key in App Settings; make sure sandbox matches your build type (debug = sandbox).
  • Android background pushes delayed — aggressive OEM battery savers (Xiaomi/Huawei…) throttle high-priority data messages for apps the user force-stopped; this affects every push SDK equally.