--- title: Remote Notification support description: 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](/fcm-mode).** It ships an automated CLI (`npx react-native-notify-kit init-nse`) that scaffolds a Swift NSE target using `NotifeeExtensionHelper`, patches your `.pbxproj` and `Podfile`, 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: ```bash 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: ```ts 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](/fcm-mode) 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: ```json { "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 ```ruby $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`). ```bash cd ios && pod install --repo-update ``` ### 3. Hook the helper into `NotificationService.m` In the generated `NotificationService.m`: ```objectivec #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 `NSURLSession` request 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 own `NSURLSession` if 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` (`mutableContent` in 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 JS `onMessage` handler to fire while the app is in the foreground. ```json { "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`: ```objectivec 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: ```js 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 notification - `EventType.ACTION_PRESS` — user tapped a Quick Action - `EventType.DISMISSED` — user explicitly dismissed the notification (only fired for notifications with a `categoryId`) Use `notification.remote` to tell whether an event came from a remote push: ```tsx 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, `DISMISSED` is only delivered for notifications that were displayed with a `categoryId`. That is a platform constraint — `UNUserNotificationCenter` only fires the dismiss callback for categorised notifications. If you need dismiss tracking on every push, ensure every payload carries a `categoryId` that matches a registered category (see [Categories & Actions](/react-native/ios/categories)). > > Background taps are routed to `onBackgroundEvent` (not `onForegroundEvent`) on **v9.2.1 and later** — earlier fork versions had a bug that routed them to foreground. See [Interaction](/react-native/ios/interaction#foreground-vs-background-routing).