Using Notifications
Schedule local notifications, register for remote push, and react to taps — all from one promise-based API.
Basic usage
Everything lives on the Notifications singleton. The four event helpers are also re-exported as standalone functions if you prefer to import them directly.
import { Notifications } from '@sigx/lynx-notifications';
Before anything will show up, the user must grant notification permission. Request it once, check the result, then schedule.
import { Notifications } from '@sigx/lynx-notifications';
async function notify() {
const { status } = await Notifications.requestPermission();
if (status !== 'granted') return;
const id = await Notifications.schedule(
{ title: 'Reminder', body: 'Check your tasks', data: { taskId: '42' } },
{ delay: 60 },
);
// keep `id` if you may want to cancel it later
return id;
}
requestPermission() shows the OS dialog when needed. On Android it delegates to @sigx/lynx-permissions (the Android 13+ runtime POST_NOTIFICATIONS prompt); on iOS it asks UNUserNotificationCenter for alert, sound, and badge. To read the current status without prompting, use getPermissionStatus().
Scheduling local notifications
schedule() resolves with a generated id you can later pass to cancel(). Cancel everything pending with cancelAll().
import { Notifications } from '@sigx/lynx-notifications';
const id = await Notifications.schedule(
{ title: 'Standup', body: 'Daily standup in 5 minutes', data: { room: 'eng' } },
{ delay: 300 },
);
// later — cancel that one
await Notifications.cancel(id);
// or clear all pending notifications
await Notifications.cancelAll();
Platform notes:
ScheduleOptions.delayis honored on iOS. On Android the current native module fires the notification immediately and does not readdelay.ScheduleOptions.repeatis declared in the type but is not honored by the current iOS or Android native code.- iOS foreground notifications show a banner and play a sound by default.
Remote push
The push flow is: request permission, subscribe to events, then register. Subscribe before you register — on iOS the device token arrives asynchronously after registration, so a listener must already be in place.
import { Notifications } from '@sigx/lynx-notifications';
async function setupPush() {
const { status } = await Notifications.requestPermission();
if (status !== 'granted') return;
// 1. token — send it to your backend (Azure Notification Hubs, Firebase Admin, etc.)
const unsubToken = Notifications.addTokenListener(async ({ token, platform }) => {
await fetch('/api/register-device', {
method: 'POST',
body: JSON.stringify({ token, platform }),
});
});
// 2. registration failures
Notifications.addTokenErrorListener(({ error }) => {
console.warn('push registration failed', error);
});
// 3. messages received while the app is running
Notifications.addPushListener((msg) => {
console.log('push received', msg.title, msg.body, msg.foreground);
});
// 4. now register
const result = await Notifications.registerForPushNotifications();
// iOS -> { dispatched: true } (token arrives via addTokenListener)
// Android -> { token, platform: 'fcm' }
if (result.error) console.warn('register error', result.error);
return unsubToken; // call to stop receiving token events
}
Every add*Listener returns an unsubscribe function. The shims are web/SSR/test-safe: with no native bridge they return a no-op unsubscribe, and a throwing handler is caught and warned so one bad listener does not break the others.
Native setup. Installing @sigx/lynx-notifications wires most of the platform plumbing on sigx prebuild: its signalx-module.json declares the aps-environment entitlement and — when you configure Firebase — the com.google.gms.google-services Gradle plugin, so you no longer patch the Gradle files or add the entitlement by hand. (The plugin is gated through the manifest's android.gradlePlugins[].requires field, set to android.googleServicesFile.) Firebase is optional: local notifications need none, and the Android build now succeeds without it (remote push stays inert until configured). What's still yours to provide:
- iOS — add the Push Notifications capability and a signing identity / provisioning profile (a paid Apple Developer account). Prebuild emits the
aps-environmententitlement —productionin Release,developmentin Debug — but the capability and signing are configured against your team. The iOS simulator cannot receive APNs pushes — use a real device. Local notifications and tap callbacks do work in the simulator. - Android — for remote push, create a Firebase project and point
android.googleServicesFileat itsgoogle-services.json; prebuild then copies it intoandroid/app/on every run and applies the Gradle plugin. Without it, the google-services plugin is skipped and the build still succeeds — add the config later and the next prebuild picks it up.
To detach later, call unregisterForPushNotifications(); it resolves with { ok, error }.
Reacting to taps and cold starts
addNotificationResponseListener fires when the user taps a notification while the app is running — remote or local on iOS, remote only on Android (see the platform note below). For a tap that cold-launched the app, call getInitialNotification() exactly once on startup.
import { Notifications } from '@sigx/lynx-notifications';
// running app
const unsub = Notifications.addNotificationResponseListener(({ notificationId, data }) => {
routeTo(data.screen);
});
// cold start (one-shot — subsequent calls return null)
const initial = await Notifications.getInitialNotification();
if (initial) routeTo(initial.data.screen);
On Android, taps currently route to addNotificationResponseListener only for remote notifications delivered through the Firebase messaging service; local notifications scheduled via schedule() do not yet route taps on Android. When the payload carries a data.notification_id, the tap response's notificationId reports that value (rather than the system-assigned request id), so the tap contract holds for remote pushes.
Conversation stacking (Android)
For chat-style apps, Android can render a conversation in the tray instead of replacing the last message. Set data.style: "messaging" on the push, and each notification sharing a data.notification_id accumulates — appending a line (the last several are kept) rather than overwriting the body — through Android's MessagingStyle.
Supporting data keys (each accepts snake_case or camelCase):
data.sender_name/senderName— attributes the line; falls back totitle.data.conversation_title/conversationTitle— the header; carried over from earlier pushes when omitted.data.group— bundles entries that share the value under one expandable tray group with an auto-posted summary.Notifications.cancel(group)dismisses the summary.
Plain (non-messaging) pushes keep replace-in-place, and below Android 6.0 stacking falls back to replace-in-place. The tap payload is the latest push's data, and Notifications.cancel(notification_id) clears the whole stacked entry.
On iOS there are no module keys for this — set the APNs aps.thread-id to the conversation id and the OS groups the thread natively.
Badge counts
import { Notifications } from '@sigx/lynx-notifications';
await Notifications.setBadgeCount(0); // clear the app-icon badge
const count = await Notifications.getBadgeCount();
Badging is iOS-only. On Android setBadgeCount is a no-op and getBadgeCount always resolves 0. On Android, to clear delivered notifications use cancelAll().
Notes
- Check
Notifications.isAvailable()to confirm the native module is present in the current build before calling into it. - See the API reference for every export, and Installation for project setup.
