Lynx/Modules/Notifications/Usage
@sigx/lynx-notifications · Beta

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.

TSX
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.

TSX
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().

TSX
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.delay is honored on iOS. On Android the current native module fires the notification immediately and does not read delay.
  • ScheduleOptions.repeat is 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.

TSX
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-environment entitlement — production in Release, development in 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.googleServicesFile at its google-services.json; prebuild then copies it into android/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.

TSX
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 to title.
  • 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#

TSX
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.