Remote Notification support
Display remote (APNs / FCM) notifications with full Notify Kit control via a Notification Service Extension.
react-native-notify-kit supports remote (push) notifications through Apple Push Notification service (APNs). Because iOS displays alert-style APNs pushes through the OS itself, any client-side rewriting (attachments, category, interruption level, custom sound from a bundled resource, …) must happen inside a Notification Service Extension (NSE) — a separate bundle that the OS runs when the push arrives. This page explains how to set one up.
The recommended path is FCM Mode. It ships an automated CLI (
npx react-native-notify-kit init-nse) that scaffolds a Swift NSE target usingNotifeeExtensionHelper, patches your.pbxprojandPodfile, and pairs with a server SDK (react-native-notify-kit/server) that builds the correct APNs payload for you. The manual Objective-C pattern documented further down is kept for apps that need custom NSE logic or have an existing integration that is not yet migrated.
Recommended: FCM Mode
The CLI + server SDK combination is a full, tested replacement for the manual setup below:
npx react-native-notify-kit init-nse
This generates a NotifyKitNSE target with a Swift NotificationService.swift that calls NotifeeExtensionHelper.populateNotificationContent on every incoming push, plus [NotifyKitNSE] NSLog diagnostics visible in Console.app. On the server side:
import { buildNotifyKitPayload } from 'react-native-notify-kit/server';
import * as admin from 'firebase-admin';
const message = buildNotifyKitPayload({
token: deviceToken,
notification: {
id: 'order-42',
title: 'Your order is on the way',
body: 'Tap to see live tracking.',
ios: {
categoryId: 'ORDER_UPDATE',
sound: 'default',
interruptionLevel: 'timeSensitive',
attachments: [{ url: 'https://cdn.example.com/orders/42.png' }],
},
},
});
await admin.messaging().send(message);
The server SDK sets mutable-content: 1 automatically so the NSE always fires, duplicates title/body into aps.alert so the OS can display a banner before the NSE finishes, and writes the notifee_options blob into apns.payload.
On the client, notifee.handleFcmMessage(remoteMessage) is a no-op on iOS in background/killed (the NSE already displayed the notification) and a foreground banner otherwise. See the full FCM Mode guide for architecture, payload schema, and troubleshooting.
Using APNs keys only (no Notify Kit-side rewriting)
If you don't need attachment, category, sound-file, or interruption-level rewriting on the client, you can send a plain APNs payload and iOS will display it directly — no NSE required:
{
"notification": { "title": "A notification title!", "body": "A notification body" },
"apns": {
"payload": {
"aps": {
"category": "post",
"sound": "media/kick.wav"
}
}
}
}
This works, but you cannot use Notify Kit's iOS-specific features (attachments from arbitrary URLs, categoryId routing to a library-registered category, custom interruptionLevel, foregroundPresentationOptions) on the alert itself without an NSE.
Manual NSE setup (legacy)
For apps that need custom NSE logic — or an existing integration that can't adopt the CLI — you can still wire up the NSE by hand. The steps below produce an NSE target that invokes the same NotifeeExtensionHelper that FCM Mode uses.
1. Add the Notification Service Extension target
- Xcode → File → New → Target…
- Select Notification Service Extension, click Next
- Product name: e.g.
NotifeeNotificationService - Make sure the new target's deployment target matches your app's (at least iOS 15.1, matching
RNNotifee.podspec)
2. Wire the Podfile
$NotifeeExtension = true
target 'NotifeeNotificationService' do
pod 'RNNotifeeCore', :path => '../node_modules/react-native-notify-kit/RNNotifeeCore.podspec'
end
$NotifeeExtension = true tells the main RNNotifee pod to exclude NotifeeExtensionHelper.{h,m} so the extension target owns them (avoids the v9.1.22 duplicate-symbols linker error with use_frameworks! :linkage => :static).
cd ios && pod install --repo-update
3. Hook the helper into NotificationService.m
In the generated NotificationService.m:
#import "NotificationService.h"
#import "NotifeeExtensionHelper.h"
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request
withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
[NotifeeExtensionHelper populateNotificationContent:request
withContent:self.bestAttemptContent
withContentHandler:contentHandler];
}
- (void)serviceExtensionTimeWillExpire {
// Safety net: hand back whatever we have if we ran out of time.
if (self.contentHandler && self.bestAttemptContent) {
self.contentHandler(self.bestAttemptContent);
}
}
@end
Build the app once and confirm it still compiles with the NSE target selected.
Notify Kit's NSE helper/core path is designed to compile inside an app extension target. Current versions keep app-only APIs out of the code compiled by the extension.
The CLI-generated Swift NSE also caps the
NSURLSessionrequest and resource timeouts at 25 s (per v9.4.0) to leave a ~5 s margin before iOS's 30 s NSE budget expires. If you use the manual Objective-C pattern above, consider setting the same cap on your ownNSURLSessionif you fetch large attachments.
4. Choose the payload
Notify Kit reads notifee_options from apns.payload (not from aps) and reshapes the notification before the OS displays it.
mutable-content: 1(mutableContentin Firebase Admin SDK) is required on every payload that should trigger the NSE. Without it, iOS will not invoke the extension.
content-available: 1(contentAvailable) is required if you also want the JSonMessagehandler to fire while the app is in the foreground.
{
"notification": { "title": "A notification title!", "body": "A notification body" },
"apns": {
"payload": {
"aps": {
"mutableContent": 1,
"contentAvailable": 1
},
"notifee_options": {
"ios": {
"sound": "media/kick.wav",
"categoryId": "post",
"attachments": [{ "url": "https://placeimg.com/640/480/any", "thumbnailHidden": true }]
}
}
}
}
}
notifee_options accepts most Notification and NotificationIOS fields. Do not include a client-side foregroundPresentationOptions here — the NSE cannot configure foreground presentation (the NSE runs only when the app is not already displaying the notification itself). If both ios.attachments and a top-level image are present, ios.attachments takes precedence.
Alternatively you can mutate bestAttemptContent.userInfo[@"notifee_options"] from inside didReceiveNotificationRequest before handing off to NotifeeExtensionHelper:
NSMutableDictionary *userInfoDict = [self.bestAttemptContent.userInfo mutableCopy];
NSMutableDictionary *opts = [NSMutableDictionary dictionary];
opts[@"title"] = @"Modified Title";
userInfoDict[@"notifee_options"] = opts;
self.bestAttemptContent.userInfo = userInfoDict;
The id of the displayed notification is always request.identifier (iOS does not let the NSE override it). Omit id from notifee_options.
Disabling Notify Kit's remote-notification delegate
By default, the library swizzles the UNUserNotificationCenter delegate so tap events for remote notifications flow through Notify Kit's event system. If you are using @react-native-firebase/messaging (or another library) and want its original onNotificationOpenedApp() / getInitialNotification() to handle taps instead, opt out at startup:
import notifee from 'react-native-notify-kit';
await notifee.setNotificationConfig({
ios: { handleRemoteNotifications: false },
});
When disabled, Notify Kit no longer intercepts remote-notification tap callbacks. Local notifications, trigger notifications, and NSE enrichment continue to work unchanged — only the delegate-forwarding path is relaxed. Call this once, early in startup, before any notifications are displayed.
Handling events
Notify Kit emits the same event types for remote notifications that it does for local ones:
EventType.PRESS— user tapped the notificationEventType.ACTION_PRESS— user tapped a Quick ActionEventType.DISMISSED— user explicitly dismissed the notification (only fired for notifications with acategoryId)
Use notification.remote to tell whether an event came from a remote push:
import { useEffect } from 'react';
import notifee, { EventType } from 'react-native-notify-kit';
export default function App() {
useEffect(() => {
return notifee.onForegroundEvent(({ type, detail }) => {
if (detail.notification?.remote) {
console.log('Remote notification:', detail.notification);
}
if (type === EventType.PRESS) {
// ...
} else if (type === EventType.DISMISSED) {
// ...
}
});
}, []);
return null;
}
On iOS,
DISMISSEDis only delivered for notifications that were displayed with acategoryId. That is a platform constraint —UNUserNotificationCenteronly fires the dismiss callback for categorised notifications. If you need dismiss tracking on every push, ensure every payload carries acategoryIdthat matches a registered category (see Categories & Actions).Background taps are routed to
onBackgroundEvent(notonForegroundEvent) on v9.2.1 and later — earlier fork versions had a bug that routed them to foreground. See Interaction.
