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
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" });
| Mistake | What 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
| Feature | Status |
|---|---|
| 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.