React Native & Expo

Pure TypeScript. No native module of its own, so there is nothing to link, no pod, no Gradle change and no config plugin.

npm install @notibase/react-native

# Recommended. Without it the device is remembered only until the app closes.
npx expo install @react-native-async-storage/async-storage

It works in Expo Go, in a development build, and in bare React Native without a prebuild — because it adds no native code. Notibase is the audience, targeting and reporting half; displaying notifications and asking for permission stay with expo-notifications or @react-native-firebase/messaging, which your app already has.

Start it

import { Notibase } from "@notibase/react-native";

await Notibase.configure("ck_live_…");   // publishable by design
A server key (sk_…) throws here rather than being accepted. Anything in an app bundle is public, and a server key is full account access — it is the one mistake in this SDK worth crashing over.

The token, and the two ways to get it wrong

This is the part worth reading twice. Both mistakes register a device that looks perfectly healthy and never receives anything.

// Expo — the DEVICE token, not the Expo push token.
import * as Notifications from "expo-notifications";
const { data } = await Notifications.getDevicePushTokenAsync();
await Notibase.registerPushToken(data);

// @react-native-firebase/messaging — note the source.
import messaging from "@react-native-firebase/messaging";
const token = await messaging().getToken();
await Notibase.registerPushToken(token, { source: "firebase" });
MistakeWhat happens
getExpoPushTokenAsync() instead of getDevicePushTokenAsync() An ExponentPushToken[…] addresses Expo’s own push service, which we do not send through — we send on your Firebase project and your APNs key. The SDK refuses it by name rather than registering a device that can never be reached.
A Firebase token on iOS with no source @react-native-firebase returns an FCM token on iOS, not an APNs one. Sent to APNs it is a BadDeviceToken on every message, forever. source: "firebase" records the channel that can actually reach it. expo-notifications gives the real APNs token, which is why the default is the other way.

Everything else

// Who this is. Everything they own becomes one audience member, and their
// inbox follows them across all of it. Call it when they sign in.
await Notibase.identify("user-42", { attributes: { plan: "pro" } });

// Events. `purchase` feeds attribution revenue.
await Notibase.track("viewed_cart", { items: 3 });
await Notibase.trackPurchase(499, "USD");        // minor units

// Attribution: the URL that opened the app, if any.
await Notibase.handleDeepLink(url);

// Click tracking: the notification's own data payload.
await Notibase.trackNotificationOpen(remoteMessage.data);

// The in-app inbox.
const { items, unread } = (await Notibase.inbox()) ?? { items: [], unread: 0 };
await Notibase.inboxMarkRead(items.map((i) => i.id));

await Notibase.unsubscribe();

Deep links and click ids

A Notibase campaign link puts nb_click on the URL that opens your app, and that id is what makes install attribution deterministic rather than a guess from an IP and a time window. Hand every incoming URL to handleDeepLink:

import * as Linking from "expo-linking";

// Cold start from a link…
Linking.getInitialURL().then((url) => Notibase.handleDeepLink(url));
// …and while running.
Linking.addEventListener("url", ({ url }) => Notibase.handleDeepLink(url));

Both firing for the same launch is normal and is handled: the same click id is acted on once, and it is attached to the install it produced rather than to every session afterwards. The value is parsed without URL, because React Native’s polyfill has not reliably parsed custom schemes and a custom scheme is exactly how a deep link arrives.

Nothing here can crash your app

Every method swallows its own network failures and warns to the console. An unhandled rejection inside a lifecycle hook is a crash on both platforms, and a notification SDK is never worth one.

Two things throw, both before any network call, both because they otherwise fail silently forever: a server key, and an Expo push token.

Storage

The device id has to survive a restart, and React Native has no localStorage. @react-native-async-storage/async-storage is used when it is installed — an optional peer dependency, so the promise of no native module holds for anyone who does not want it.

Without it the SDK works for the length of one launch and says so loudly, because the failure is otherwise invisible in the worst way: every call succeeds, and every cold start of every install registers a new device. Any adapter with getItem/setItem/removeItem returning promises works — expo-secure-store, MMKV, your own:

await Notibase.configure("ck_live_…", { storage: myAdapter });

Is it actually wired up?

if (__DEV__) console.table(await Notibase.runSetupTest());

Most of what goes wrong during an integration cannot be seen from inside the app: credentials that were never uploaded, an APNs key minted for a different bundle, a device that never registered. This reports what the app can see and returns what the server makes of it. With no native module there is no bundle id and no permission status to send, so the server leaves out the checks it has no input for rather than inventing them.

In-app messages

A message shown inside your app rather than sent to it. You publish a rule in the console; this SDK caches it and the device decides when to show it — on app open, on foreground, or when your app puts a value in front of it. The full behaviour is on In-app messages.

import { Notibase, NotibaseInAppMessages } from "@notibase/react-native";

await Notibase.enableInAppMessages({
  // An "Ask for push permission" button calls this. The SDK cannot ask by
  // itself — permission belongs to the library you already have.
  onPromptPush: () => Notifications.requestPermissionsAsync(),
  // Optional: keep a campaign link inside the app rather than in the browser.
  onOpenUrl: (url) => navigation.navigate(url),
});

// Then mount this once, near the root of your app.
export default function App() {
  return (
    <NavigationContainer>
      <Routes />
      <NotibaseInAppMessages />
    </NavigationContainer>
  );
}

You place the component yourself because this package owns no native module and no part of your view tree. That is not a limitation to work around — it is why your theme, your back button and your accessibility settings apply to a message without any of it being reinvented. Enable in-app messages and forget to mount it and the SDK says so in the console, rather than showing nothing forever.

// Fires a campaign configured for "cart_value over 100", now.
Notibase.setTrigger("cart_value", 240);

A trigger is a local fact and never leaves the device. Values are compared without coercion, the same way on every SDK: a trigger set to the string "100" does not satisfy over 100.

A message is a document of text, image, button and spacer blocks, drawn with ordinary React Native components. There is no WebView and no dangerouslySetInnerHTML anywhere in it, which is the whole reason the content is blocks rather than HTML somebody typed into a console.

What is not here

FeatureStatus
Showing notifications, permission prompts, notification channels Deliberately not ours. expo-notifications and @react-native-firebase/messaging do this well and you already have one of them; wrapping them would add a native module for no gain.

Moving from OneSignal

Your device tokens come with you — they were issued by your Firebase project and your Apple team, not by a vendor. Moving from OneSignal maps every API field and SDK call to its equivalent, and says which subscribers cannot travel.

← FlutterREST API →