react-native-notify-kit 9.6.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -5
- package/RNNotifeeCore.podspec +2 -0
- package/android/src/main/java/app/notifee/core/NotificationManager.java +98 -0
- package/android/src/test/java/app/notifee/core/GetDisplayedNotificationsDataTest.java +242 -0
- package/cli/bin/react-native-notify-kit +2 -0
- package/cli/dist/cli.bundle.js +20834 -0
- package/cli/dist/cli.d.ts +2 -0
- package/cli/dist/cli.js +87 -0
- package/cli/dist/cli.js.map +1 -0
- package/cli/dist/commands/initNse.d.ts +12 -0
- package/cli/dist/commands/initNse.js +212 -0
- package/cli/dist/commands/initNse.js.map +1 -0
- package/cli/dist/lib/detectProject.d.ts +15 -0
- package/cli/dist/lib/detectProject.js +156 -0
- package/cli/dist/lib/detectProject.js.map +1 -0
- package/cli/dist/lib/logger.d.ts +4 -0
- package/cli/dist/lib/logger.js +24 -0
- package/cli/dist/lib/logger.js.map +1 -0
- package/cli/dist/lib/patchPodfile.d.ts +12 -0
- package/cli/dist/lib/patchPodfile.js +143 -0
- package/cli/dist/lib/patchPodfile.js.map +1 -0
- package/cli/dist/lib/patchXcodeProject.d.ts +25 -0
- package/cli/dist/lib/patchXcodeProject.js +239 -0
- package/cli/dist/lib/patchXcodeProject.js.map +1 -0
- package/cli/dist/lib/writeTemplates.d.ts +11 -0
- package/cli/dist/lib/writeTemplates.js +82 -0
- package/cli/dist/lib/writeTemplates.js.map +1 -0
- package/cli/dist/templates/Info.plist.tmpl +29 -0
- package/cli/dist/templates/NotificationService.swift.tmpl +32 -0
- package/cli/dist/templates/NotifyKitNSE.entitlements.tmpl +6 -0
- package/cli/dist/templates/templates/Info.plist.tmpl +29 -0
- package/cli/dist/templates/templates/NotificationService.swift.tmpl +73 -0
- package/cli/dist/templates/templates/NotifyKitNSE.entitlements.tmpl +6 -0
- package/dist/NotifeeApiModule.d.ts +17 -0
- package/dist/NotifeeApiModule.js +66 -1
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/fcm/index.d.ts +4 -0
- package/dist/fcm/index.js +3 -0
- package/dist/fcm/index.js.map +1 -0
- package/dist/fcm/parseFcmPayload.d.ts +16 -0
- package/dist/fcm/parseFcmPayload.js +37 -0
- package/dist/fcm/parseFcmPayload.js.map +1 -0
- package/dist/fcm/reconstructNotification.d.ts +8 -0
- package/dist/fcm/reconstructNotification.js +167 -0
- package/dist/fcm/reconstructNotification.js.map +1 -0
- package/dist/fcm/types.d.ts +51 -0
- package/dist/fcm/types.js +6 -0
- package/dist/fcm/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/Module.d.ts +21 -0
- package/dist/types/Notification.d.ts +35 -4
- package/dist/types/Notification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/jest-mock.js +1 -0
- package/package.json +38 -3
- package/server/README.md +129 -0
- package/server/dist/android.d.ts +8 -0
- package/server/dist/android.d.ts.map +1 -0
- package/server/dist/android.js +20 -0
- package/server/dist/android.js.map +1 -0
- package/server/dist/buildPayload.d.ts +3 -0
- package/server/dist/buildPayload.d.ts.map +1 -0
- package/server/dist/buildPayload.js +82 -0
- package/server/dist/buildPayload.js.map +1 -0
- package/server/dist/index.d.ts +6 -0
- package/server/dist/index.d.ts.map +1 -0
- package/server/dist/index.js +12 -0
- package/server/dist/index.js.map +1 -0
- package/server/dist/ios.d.ts +11 -0
- package/server/dist/ios.d.ts.map +1 -0
- package/server/dist/ios.js +61 -0
- package/server/dist/ios.js.map +1 -0
- package/server/dist/serialize.d.ts +11 -0
- package/server/dist/serialize.d.ts.map +1 -0
- package/server/dist/serialize.js +32 -0
- package/server/dist/serialize.js.map +1 -0
- package/server/dist/types.d.ts +2 -0
- package/server/dist/types.d.ts.map +1 -0
- package/server/dist/types.js +3 -0
- package/server/dist/types.js.map +1 -0
- package/server/dist/validation.d.ts +3 -0
- package/server/dist/validation.d.ts.map +1 -0
- package/server/dist/validation.js +101 -0
- package/server/dist/validation.js.map +1 -0
- package/server/package.json +5 -0
- package/server/src/android.ts +27 -0
- package/server/src/buildPayload.ts +91 -0
- package/server/src/index.ts +12 -0
- package/server/src/ios.ts +88 -0
- package/server/src/serialize.ts +39 -0
- package/server/src/types.ts +24 -0
- package/server/src/validation.ts +134 -0
- package/server/tsconfig.json +25 -0
- package/src/NotifeeApiModule.ts +80 -1
- package/src/fcm/index.ts +4 -0
- package/src/fcm/parseFcmPayload.ts +56 -0
- package/src/fcm/reconstructNotification.ts +201 -0
- package/src/fcm/types.ts +57 -0
- package/src/index.ts +2 -0
- package/src/internal/fcmContract.d.ts +150 -0
- package/src/types/Module.ts +23 -0
- package/src/types/Notification.ts +35 -4
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -135,10 +135,30 @@ useEffect(() => {
|
|
|
135
135
|
}, []);
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
## Notifee FCM Mode (NEW in 10.0.0)
|
|
139
|
+
|
|
140
|
+
**Use `react-native-notify-kit` as the sole FCM display layer on both Android and iOS** — one developer API, no duplicate notifications on Android, no silent-push drops on iOS. Ship a server SDK payload, let the client handle it in one line, and scaffold the iOS Notification Service Extension with a single CLI command:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# server: build the payload
|
|
144
|
+
import { buildNotifyKitPayload } from 'react-native-notify-kit/server';
|
|
145
|
+
await admin.messaging().send(buildNotifyKitPayload({ token, notification: { title, body, android, ios } }));
|
|
146
|
+
|
|
147
|
+
# client (Android + iOS): one line in setBackgroundMessageHandler / onMessage
|
|
148
|
+
await notifee.handleFcmMessage(remoteMessage);
|
|
149
|
+
|
|
150
|
+
# iOS NSE scaffold
|
|
151
|
+
npx react-native-notify-kit init-nse && cd ios && pod install
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
See the full guide: **[docs/fcm-mode.md](docs/fcm-mode.md)** — covers architecture, server SDK reference, client API, NSE setup, payload schema, migration, troubleshooting, and known limitations.
|
|
155
|
+
|
|
138
156
|
## Push Notifications (Firebase)
|
|
139
157
|
|
|
140
158
|
This library handles notification **display and management**. For receiving push notifications, pair it with [`@react-native-firebase/messaging`](https://rnfirebase.io/messaging/usage):
|
|
141
159
|
|
|
160
|
+
> **New in 10.0.0:** for a turnkey FCM integration that handles both platforms (no duplicate on Android, APNs-reliable on iOS), use **[Notifee FCM Mode](docs/fcm-mode.md)** instead of the manual pattern below. The sections that follow still apply for basic Firebase setup (google-services plugin, permissions, APNs capability).
|
|
161
|
+
|
|
142
162
|
### Android setup
|
|
143
163
|
|
|
144
164
|
1. Add Firebase dependencies to your app:
|
|
@@ -196,7 +216,22 @@ messaging().onMessage(async remoteMessage => {
|
|
|
196
216
|
|
|
197
217
|
## iOS Notification Service Extension
|
|
198
218
|
|
|
199
|
-
To modify push notification content before display (e.g., attach images), create a Notification Service Extension
|
|
219
|
+
To modify push notification content before display (e.g., attach images), create a Notification Service Extension.
|
|
220
|
+
|
|
221
|
+
### Automated setup (recommended)
|
|
222
|
+
|
|
223
|
+
From your project root, with `react-native-notify-kit` installed:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
npx react-native-notify-kit init-nse
|
|
227
|
+
cd ios && pod install
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The CLI scaffolds a Swift NSE target (default name: `NotifyKitNSE`), patches your Podfile, and wires `.pbxproj`. Open your `.xcworkspace` in Xcode, verify the NSE target's signing, and build. See [docs/fcm-mode.md#ios-nse-setup](docs/fcm-mode.md#ios-nse-setup) for the full CLI reference (target name, bundle suffix, `--dry-run`, `--force`).
|
|
231
|
+
|
|
232
|
+
### Manual setup
|
|
233
|
+
|
|
234
|
+
For Expo, monorepos with non-standard iOS paths, or heavily-customized Xcode configurations where the CLI can't patch the project cleanly:
|
|
200
235
|
|
|
201
236
|
1. In Xcode: **File > New > Target > Notification Service Extension**
|
|
202
237
|
2. Add to your Podfile:
|
|
@@ -234,6 +269,40 @@ To modify push notification content before display (e.g., attach images), create
|
|
|
234
269
|
|
|
235
270
|
5. Run `cd ios && pod install`
|
|
236
271
|
|
|
272
|
+
## Server SDK
|
|
273
|
+
|
|
274
|
+
`react-native-notify-kit` ships a **zero-dependency server SDK** under the `/server` subpath, for building FCM HTTP v1 payloads that the client `handleFcmMessage` handler consumes. Runs in Node.js 22+ and Firebase Cloud Functions.
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { buildNotifyKitPayload } from 'react-native-notify-kit/server';
|
|
278
|
+
import * as admin from 'firebase-admin';
|
|
279
|
+
|
|
280
|
+
const message = buildNotifyKitPayload({
|
|
281
|
+
token: '<device FCM token>',
|
|
282
|
+
notification: {
|
|
283
|
+
id: 'order-42',
|
|
284
|
+
title: 'Your order is on the way',
|
|
285
|
+
body: 'Tap to see live tracking',
|
|
286
|
+
data: { orderId: '42' },
|
|
287
|
+
android: { channelId: 'orders', smallIcon: 'ic_notification' },
|
|
288
|
+
ios: { sound: 'default', interruptionLevel: 'timeSensitive' },
|
|
289
|
+
},
|
|
290
|
+
options: { androidPriority: 'high', ttl: 3600 },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await admin.messaging().send(message);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Full reference: [docs/fcm-mode.md#server-sdk-reference](docs/fcm-mode.md#server-sdk-reference) (types, validation rules, payload shape, FCM 4 KB limit).
|
|
297
|
+
|
|
298
|
+
## CLI Tools
|
|
299
|
+
|
|
300
|
+
The library ships a small CLI at `npx react-native-notify-kit`. Currently one command is available:
|
|
301
|
+
|
|
302
|
+
- `npx react-native-notify-kit init-nse` — scaffolds an iOS Notification Service Extension (Swift), patches the Podfile, and wires `.pbxproj`. See [docs/fcm-mode.md#ios-nse-setup](docs/fcm-mode.md#ios-nse-setup) for options.
|
|
303
|
+
|
|
304
|
+
The CLI is prepacked into the main package at publish time, so `npx react-native-notify-kit` works immediately after `yarn add react-native-notify-kit` — no separate install.
|
|
305
|
+
|
|
237
306
|
## Jest Testing
|
|
238
307
|
|
|
239
308
|
Mock the native module in your Jest setup file:
|
|
@@ -263,7 +332,7 @@ This fork is a complete migration to React Native's **New Architecture**:
|
|
|
263
332
|
- **Toolchain**: Yarn 4, Node 22+, Java 17, compileSdk/targetSdk 35
|
|
264
333
|
- **Single Android module** — the original Notifee shipped a pre-compiled AAR bundled inside the npm tarball under a frozen Maven coordinate; this fork compiles the core from source as part of the React Native bridge module on every consumer build. Eliminates the `FAIL_ON_PROJECT_REPOS` issue on RN 0.74+ and the Gradle cache staleness bug that could serve outdated bytecode after `yarn upgrade`.
|
|
265
334
|
- **Core notification logic (NotifeeCore) is unchanged** — the public API is fully compatible with the original Notifee
|
|
266
|
-
- **
|
|
335
|
+
- **33 upstream bugs fixed** — see [Bugs Fixed from Upstream Notifee](#bugs-fixed-from-upstream-notifee) below
|
|
267
336
|
- **Reliable trigger notifications** — AlarmManager is the default backend instead of WorkManager, with automatic fallback when exact alarm permission is not granted
|
|
268
337
|
- **New API: `setNotificationConfig()`** — opt-out flag to prevent Notifee from intercepting iOS remote notification handlers (see [New APIs](#new-apis) below)
|
|
269
338
|
- **Baseline Profile** — the library AAR ships a Baseline Profile that instructs ART to AOT-compile the foreground service notification hot path at install time, eliminating JIT penalty on first invocation
|
|
@@ -304,8 +373,21 @@ This fork fixes the following bugs that were never resolved in the original Noti
|
|
|
304
373
|
| `getBadgeCount:` completion block never called when running in an app extension, causing JS promises to hang forever in NSE handlers | iOS | Pre-existing | 9.4.0 |
|
|
305
374
|
| Notification Service Extension attachment downloads had no timeout cap (default 60-second `NSURLSession` timeout exceeds iOS's ~30-second NSE budget), causing extension process kill and notification loss on slow networks | iOS | Pre-existing | 9.4.0 |
|
|
306
375
|
| `cancelTriggerNotifications()` / `createTriggerNotification()` promises resolve before Room DB write completes, causing ~3% race on cancel-then-create patterns. Also fixes a previously-undocumented reboot-recovery data-loss bug in `NotifeeAlarmManager.rescheduleNotification` and an ordering bug in `NotificationManager.doScheduledWork` | Android | [#549](https://github.com/invertase/notifee/issues/549) | 9.5.0 |
|
|
307
|
-
| Scheduled trigger notifications silently lost across device reboot on OEM devices (Xiaomi MIUI, OnePlus, Huawei EMUI, Oppo ColorOS, Vivo FuntouchOS) whose vendor OS suppresses `BOOT_COMPLETED` until the user manually enables autostart. Also handles zombie non-repeating triggers whose fire time already passed (fire-once within a 24-hour grace period, then delete the Room row; delete silently beyond the grace period) and adds try/catch/finally guards to all notifee `BroadcastReceiver` async paths. | Android | [#734](https://github.com/invertase/notifee/issues/734) |
|
|
308
|
-
| `ObjectAlreadyConsumedException` in headless task when the same `WritableMap` is reused or the `taskConfig` accessor is read twice — `TaskConfig.init` mutated the caller's map instead of copying it first. Latent in most apps but observed in production by upstream users with high-frequency headless events | Android | [#266](https://github.com/invertase/notifee/issues/266) |
|
|
376
|
+
| Scheduled trigger notifications silently lost across device reboot on OEM devices (Xiaomi MIUI, OnePlus, Huawei EMUI, Oppo ColorOS, Vivo FuntouchOS) whose vendor OS suppresses `BOOT_COMPLETED` until the user manually enables autostart. Also handles zombie non-repeating triggers whose fire time already passed (fire-once within a 24-hour grace period, then delete the Room row; delete silently beyond the grace period) and adds try/catch/finally guards to all notifee `BroadcastReceiver` async paths. | Android | [#734](https://github.com/invertase/notifee/issues/734) | 9.6.0 |
|
|
377
|
+
| `ObjectAlreadyConsumedException` in headless task when the same `WritableMap` is reused or the `taskConfig` accessor is read twice — `TaskConfig.init` mutated the caller's map instead of copying it first. Latent in most apps but observed in production by upstream users with high-frequency headless events | Android | [#266](https://github.com/invertase/notifee/issues/266) | 9.6.0 |
|
|
378
|
+
| `getDisplayedNotifications()` returned no `data` field on Android, breaking iOS/Android API symmetry where iOS exposes custom keys via `parseDataFromUserInfo:` (see platform limitation note below — the fix is API parity for app-posted notifications, not a workaround for FCM background auto-display) | Android | [#393](https://github.com/invertase/notifee/issues/393) | 9.7.0 |
|
|
379
|
+
|
|
380
|
+
> **Important note on `getDisplayedNotifications()` and FCM custom data on Android.**
|
|
381
|
+
>
|
|
382
|
+
> The 9.7.0 fix for [#393](https://github.com/invertase/notifee/issues/393) makes `getDisplayedNotifications()` expose a `data` field for notifications stored in `Notification.extras` (matching iOS shape). However, when the FCM Android SDK auto-displays a `notification`+`data` push while the app is in background or killed, custom `data` fields are placed only on the tap-action `PendingIntent` — never on `Notification.extras`. This is the FCM SDK's original design (see `CommonNotificationBuilder.createContentIntent` in [firebase-android-sdk](https://github.com/firebase/firebase-android-sdk/blob/master/firebase-messaging/src/main/java/com/google/firebase/messaging/CommonNotificationBuilder.java)) and is not workaroundable from any library — the PendingIntent's extras are sealed by Android's security model. Firebase tracks this gap in [firebase-android-sdk#2639](https://github.com/firebase/firebase-android-sdk/issues/2639) (open since 2021).
|
|
383
|
+
>
|
|
384
|
+
> **The fix does work** for these scenarios on Android: notifications created via `notifee.displayNotification({ data: {...} })`, FCM data-only messages handled in `onMessageReceived` followed by an explicit `displayNotification()` call, custom `FirebaseMessagingService.handleIntent` overrides that inject extras before display, and notifications from other libraries that call `NotificationCompat.Builder.addExtras(bundle)`.
|
|
385
|
+
>
|
|
386
|
+
> **Recommended pattern for full control over FCM push notifications on Android**: send FCM data-only messages (omit the `notification` field server-side), handle them in `onMessageReceived` (or a headless task in killed state), and render the notification yourself via `notifee.displayNotification()`. This gives full control over `data`, channel, styling, tap behavior, and is also Firebase's [official recommendation](https://firebase.blog/posts/2018/09/handle-fcm-messages-on-android/).
|
|
387
|
+
>
|
|
388
|
+
> **Reserved keys filtered from `data`** (custom payload keys matching these are dropped): prefixes `android.`, `google.`, `gcm.`, `fcm.` (with trailing dot — `fcmRegion`, `googleish` survive), `notifee` (no trailing dot — library's reserved namespace, `notifeeFoo` is also filtered), plus exact keys `from`, `collapse_key`, `message_type`, `message_id`, `aps`, `fcm_options`. The `fcm_options` exact-match matches iOS behavior on the Firebase analytics-label key. **Cross-platform note**: bare-`fcm` keys other than `fcm_options` (e.g. `fcmRegion`, `fcmToken`) are preserved on Android but filtered on iOS — rename server-side if you need strict parity.
|
|
389
|
+
|
|
390
|
+
<!-- markdownlint-disable-line MD028 -->
|
|
309
391
|
|
|
310
392
|
> **Note for apps requiring guaranteed exact alarms (alarm clocks, timers, calendars):**
|
|
311
393
|
> Add `<uses-permission android:name="android.permission.USE_EXACT_ALARM" />` to your app's
|
|
@@ -675,7 +757,7 @@ The script runs the macrobenchmark test in `apps/smoke/android/baselineprofile/`
|
|
|
675
757
|
|
|
676
758
|
## Documentation
|
|
677
759
|
|
|
678
|
-
The
|
|
760
|
+
The full `react-native-notify-kit` documentation is hosted on docs.page and is kept in sync with this repo. It covers the public API, platform guides, FCM Mode, the server SDK, and the `init-nse` CLI.
|
|
679
761
|
|
|
680
762
|
- [Overview](https://docs.page/marcocrupi/react-native-notify-kit/react-native/overview)
|
|
681
763
|
- [Reference](https://docs.page/marcocrupi/react-native-notify-kit/react-native/reference)
|
package/RNNotifeeCore.podspec
CHANGED
|
@@ -16,6 +16,8 @@ Pod::Spec.new do |s|
|
|
|
16
16
|
|
|
17
17
|
s.cocoapods_version = '>= 1.10.0'
|
|
18
18
|
s.ios.deployment_target = '15.1'
|
|
19
|
+
s.module_name = 'RNNotifeeCore'
|
|
20
|
+
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
|
19
21
|
|
|
20
22
|
if defined?($NotifeeCoreFromSources) && $NotifeeCoreFromSources == true
|
|
21
23
|
# internal dev flag used by Notifee devs, ignore
|
|
@@ -72,8 +72,11 @@ import com.google.common.util.concurrent.ListenableFuture;
|
|
|
72
72
|
import com.google.common.util.concurrent.ListeningExecutorService;
|
|
73
73
|
import com.google.common.util.concurrent.MoreExecutors;
|
|
74
74
|
import java.util.ArrayList;
|
|
75
|
+
import java.util.Arrays;
|
|
76
|
+
import java.util.HashSet;
|
|
75
77
|
import java.util.List;
|
|
76
78
|
import java.util.Objects;
|
|
79
|
+
import java.util.Set;
|
|
77
80
|
import java.util.concurrent.Callable;
|
|
78
81
|
import java.util.concurrent.ExecutorService;
|
|
79
82
|
import java.util.concurrent.Executors;
|
|
@@ -84,6 +87,43 @@ class NotificationManager {
|
|
|
84
87
|
private static final String TAG = "NotificationManager";
|
|
85
88
|
private static final String EXTRA_NOTIFEE_NOTIFICATION = "notifee.notification";
|
|
86
89
|
private static final String EXTRA_NOTIFEE_TRIGGER = "notifee.trigger";
|
|
90
|
+
|
|
91
|
+
// Denylist for getDisplayedNotifications() data extraction — pairs with
|
|
92
|
+
// EXCLUDED_DATA_KEY_PREFIXES below. `fcm_options` is the Firebase HTTP v1
|
|
93
|
+
// `Message.fcm_options` analytics label; on iOS it is caught by the bare
|
|
94
|
+
// `hasPrefix:@"fcm"` filter in NotifeeCoreUtil.m:627, and we match that
|
|
95
|
+
// filter exactly on Android via this set so the analytics label never
|
|
96
|
+
// leaks into `notification.data`.
|
|
97
|
+
private static final Set<String> EXCLUDED_DATA_KEYS =
|
|
98
|
+
new HashSet<>(
|
|
99
|
+
Arrays.asList(
|
|
100
|
+
"from", "collapse_key", "message_type", "message_id", "aps", "fcm_options"));
|
|
101
|
+
|
|
102
|
+
// Prefixes that denote system or internal keys.
|
|
103
|
+
// - `android.`, `gcm.`, `google.`, `fcm.` include the trailing dot so
|
|
104
|
+
// legitimate custom keys like `androidify`, `googleish`, or `fcmRegion`
|
|
105
|
+
// survive the filter.
|
|
106
|
+
// - `notifee` is intentionally without dot: it's the library's own namespace
|
|
107
|
+
// and we reserve it entirely (matches the internal constants
|
|
108
|
+
// `notifee.notification` / `notifee.trigger`, plus `notifee_options` and
|
|
109
|
+
// any future addition — mirrors iOS NotifeeCoreUtil.m:626 which uses
|
|
110
|
+
// hasPrefix:@"notifee" for the same reason).
|
|
111
|
+
//
|
|
112
|
+
// Note: `fcm.` with the dot on Android diverges from iOS, which uses bare
|
|
113
|
+
// `hasPrefix:@"fcm"` (NotifeeCoreUtil.m:627-628). The iOS filter exists
|
|
114
|
+
// specifically to drop `fcm_options` (the Firebase HTTP v1 analytics
|
|
115
|
+
// label, documented inline in that iOS source as `// fcm_options`) — it
|
|
116
|
+
// is NOT a historical/broad match. We handle `fcm_options` via the
|
|
117
|
+
// EXCLUDED_DATA_KEYS exact-match set above, which gives us parity on the
|
|
118
|
+
// Firebase-reserved key while preserving realistic user keys like
|
|
119
|
+
// `fcmRegion` and `fcmToken` that iOS's broader filter would also drop.
|
|
120
|
+
// Apps that need strict cross-platform consistency for custom data keys
|
|
121
|
+
// should avoid names starting with `fcm` entirely (iOS drops them, Android
|
|
122
|
+
// preserves everything except the exact `fcm_options` label).
|
|
123
|
+
private static final String[] EXCLUDED_DATA_KEY_PREFIXES = {
|
|
124
|
+
"android.", "gcm.", "google.", "notifee", "fcm."
|
|
125
|
+
};
|
|
126
|
+
|
|
87
127
|
private static final ExecutorService CACHED_THREAD_POOL = Executors.newCachedThreadPool();
|
|
88
128
|
private static final ListeningExecutorService LISTENING_CACHED_THREAD_POOL =
|
|
89
129
|
MoreExecutors.listeningDecorator(CACHED_THREAD_POOL);
|
|
@@ -866,6 +906,8 @@ class NotificationManager {
|
|
|
866
906
|
notificationBundle.putString("subtitle", subtitle.toString());
|
|
867
907
|
}
|
|
868
908
|
|
|
909
|
+
notificationBundle.putBundle("data", extractDataFromExtras(extras));
|
|
910
|
+
|
|
869
911
|
Bundle androidBundle = new Bundle();
|
|
870
912
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
871
913
|
androidBundle.putString("channelId", original.getChannelId());
|
|
@@ -894,6 +936,62 @@ class NotificationManager {
|
|
|
894
936
|
});
|
|
895
937
|
}
|
|
896
938
|
|
|
939
|
+
/**
|
|
940
|
+
* Builds a {@link Bundle} of custom data keys from a {@link StatusBarNotification} extras bundle,
|
|
941
|
+
* filtering out Android system keys, FCM/Firebase internals, and Notifee-reserved keys via {@link
|
|
942
|
+
* #shouldIncludeInData(String)}. Mirrors iOS {@code parseDataFromUserInfo:} in {@code
|
|
943
|
+
* NotifeeCoreUtil.m} on the Firebase-reserved keys ({@code fcm_options}, {@code gcm.*}, {@code
|
|
944
|
+
* google.*}, {@code aps}, etc.), with a deliberate divergence: Android uses {@code fcm.} (with
|
|
945
|
+
* dot) plus an exact-match on {@code fcm_options} to preserve realistic user keys like {@code
|
|
946
|
+
* fcmRegion} and {@code fcmToken} that iOS's broader bare-{@code fcm} prefix would also drop. See
|
|
947
|
+
* {@link #EXCLUDED_DATA_KEY_PREFIXES} and {@link #EXCLUDED_DATA_KEYS} for the full rationale.
|
|
948
|
+
*
|
|
949
|
+
* <p>Values are coerced to {@code String} via {@code toString()}. FCM {@code data} payloads are
|
|
950
|
+
* {@code Map<String, String>} by contract, so the coercion is a no-op in practice; the defensive
|
|
951
|
+
* {@code toString()} call only matters for the rare non-String extra that might survive the
|
|
952
|
+
* denylist (e.g. a numeric value added via {@code Bundle.putInt}).
|
|
953
|
+
*
|
|
954
|
+
* <p>Always returns a non-null {@code Bundle} (possibly empty) so that JS consumers can
|
|
955
|
+
* dereference {@code notification.data.foo} without null-checking {@code data} itself — matching
|
|
956
|
+
* iOS, which always returns a dictionary.
|
|
957
|
+
*/
|
|
958
|
+
static Bundle extractDataFromExtras(Bundle extras) {
|
|
959
|
+
Bundle dataBundle = new Bundle();
|
|
960
|
+
if (extras == null) {
|
|
961
|
+
return dataBundle;
|
|
962
|
+
}
|
|
963
|
+
for (String key : extras.keySet()) {
|
|
964
|
+
if (!shouldIncludeInData(key)) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
Object value = extras.get(key);
|
|
968
|
+
if (value != null) {
|
|
969
|
+
dataBundle.putString(key, value.toString());
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return dataBundle;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Pure denylist predicate used by {@link #extractDataFromExtras(Bundle)}. Extracted for direct
|
|
977
|
+
* unit testing without having to construct a {@link Bundle}. See the {@link
|
|
978
|
+
* #EXCLUDED_DATA_KEY_PREFIXES} docs for the reasoning behind each prefix choice.
|
|
979
|
+
*/
|
|
980
|
+
static boolean shouldIncludeInData(String key) {
|
|
981
|
+
if (key == null) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
if (EXCLUDED_DATA_KEYS.contains(key)) {
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
for (String prefix : EXCLUDED_DATA_KEY_PREFIXES) {
|
|
988
|
+
if (key.startsWith(prefix)) {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
|
|
897
995
|
static void getTriggerNotifications(MethodCallResult<List<Bundle>> result) {
|
|
898
996
|
WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext());
|
|
899
997
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2016-present Invertase Limited & Contributors
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this library except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import static org.junit.Assert.assertEquals;
|
|
20
|
+
import static org.junit.Assert.assertFalse;
|
|
21
|
+
import static org.junit.Assert.assertNotNull;
|
|
22
|
+
import static org.junit.Assert.assertTrue;
|
|
23
|
+
|
|
24
|
+
import android.os.Bundle;
|
|
25
|
+
import java.util.Arrays;
|
|
26
|
+
import java.util.HashSet;
|
|
27
|
+
import java.util.Set;
|
|
28
|
+
import org.junit.Test;
|
|
29
|
+
import org.junit.runner.RunWith;
|
|
30
|
+
import org.robolectric.RobolectricTestRunner;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unit tests for {@link NotificationManager#shouldIncludeInData(String)} and {@link
|
|
34
|
+
* NotificationManager#extractDataFromExtras(android.os.Bundle)} — the denylist filter and
|
|
35
|
+
* bundle-to-bundle helper added for upstream invertase/notifee#393.
|
|
36
|
+
*
|
|
37
|
+
* <p>The end-to-end {@code getDisplayedNotifications()} path is intentionally not exercised here
|
|
38
|
+
* because it depends on {@code android.app.NotificationManager.getActiveNotifications()}, which
|
|
39
|
+
* would require a system service mock. Extracting the pure predicate and the extras iteration into
|
|
40
|
+
* helper methods lets these tests run under Robolectric with a real {@link Bundle} implementation
|
|
41
|
+
* and no system service at all.
|
|
42
|
+
*/
|
|
43
|
+
@RunWith(RobolectricTestRunner.class)
|
|
44
|
+
public class GetDisplayedNotificationsDataTest {
|
|
45
|
+
|
|
46
|
+
// -------- shouldIncludeInData (pure predicate) --------
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
public void shouldIncludeInData_plainKey_returnsTrue() {
|
|
50
|
+
assertTrue(NotificationManager.shouldIncludeInData("event"));
|
|
51
|
+
assertTrue(NotificationManager.shouldIncludeInData("userId"));
|
|
52
|
+
assertTrue(NotificationManager.shouldIncludeInData("conversation_id"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
public void shouldIncludeInData_androidPrefix_returnsFalse() {
|
|
57
|
+
assertFalse(NotificationManager.shouldIncludeInData("android.title"));
|
|
58
|
+
assertFalse(NotificationManager.shouldIncludeInData("android.text"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test
|
|
62
|
+
public void shouldIncludeInData_androidLookalike_returnsTrue() {
|
|
63
|
+
// The `android.` prefix intentionally includes the dot, so custom keys that
|
|
64
|
+
// merely begin with the substring `android` are not accidentally filtered.
|
|
65
|
+
assertTrue(NotificationManager.shouldIncludeInData("androidify"));
|
|
66
|
+
assertTrue(NotificationManager.shouldIncludeInData("androidish"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
public void shouldIncludeInData_googlePrefix_returnsFalse() {
|
|
71
|
+
assertFalse(NotificationManager.shouldIncludeInData("google.sent_time"));
|
|
72
|
+
assertFalse(NotificationManager.shouldIncludeInData("google.delivered_priority"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
public void shouldIncludeInData_googleLookalike_returnsTrue() {
|
|
77
|
+
// Same reasoning as androidLookalike — `google.` with dot keeps the filter precise.
|
|
78
|
+
assertTrue(NotificationManager.shouldIncludeInData("googleish"));
|
|
79
|
+
assertTrue(NotificationManager.shouldIncludeInData("googlebot"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
public void shouldIncludeInData_gcmPrefix_returnsFalse() {
|
|
84
|
+
assertFalse(NotificationManager.shouldIncludeInData("gcm.notification.foo"));
|
|
85
|
+
assertFalse(NotificationManager.shouldIncludeInData("gcm.n.e"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
public void shouldIncludeInData_notifeePrefix_returnsFalse() {
|
|
90
|
+
// "notifee.notification" and "notifee.trigger" are the current internal
|
|
91
|
+
// constants (EXTRA_NOTIFEE_NOTIFICATION / EXTRA_NOTIFEE_TRIGGER). They
|
|
92
|
+
// are caught by the no-dot `notifee` prefix.
|
|
93
|
+
assertFalse(NotificationManager.shouldIncludeInData("notifee.notification"));
|
|
94
|
+
assertFalse(NotificationManager.shouldIncludeInData("notifee.trigger"));
|
|
95
|
+
|
|
96
|
+
// `notifee` is INTENTIONALLY kept without the dot: the library reserves
|
|
97
|
+
// its own namespace entirely, so `notifeeFoo` is also filtered. This
|
|
98
|
+
// differs from `fcm.` (with dot, see below): for `notifee` the whole
|
|
99
|
+
// prefix is library-owned and can collide with future internal constants,
|
|
100
|
+
// whereas `fcm` is a third-party namespace where we prefer to let
|
|
101
|
+
// realistic user keys like `fcmRegion` or `fcmToken` survive.
|
|
102
|
+
assertFalse(NotificationManager.shouldIncludeInData("notifeeFoo"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
public void shouldIncludeInData_fcmDottedPrefix_returnsFalse() {
|
|
107
|
+
// `fcm.` uses the trailing dot so realistic user custom keys (fcmRegion,
|
|
108
|
+
// fcmToken, fcmlike) can round-trip through `data`. This diverges from
|
|
109
|
+
// iOS parseDataFromUserInfo:, which uses bare `[key hasPrefix:@"fcm"]`
|
|
110
|
+
// specifically to catch `fcm_options` (see the `// fcm_options` marker
|
|
111
|
+
// at NotifeeCoreUtil.m:627-628 and the dedicated
|
|
112
|
+
// shouldIncludeInData_fcmOptionsExactKey_returnsFalse test above). Both
|
|
113
|
+
// platforms drop `fcm_options`; Android additionally preserves bare-fcm
|
|
114
|
+
// user keys (fcmRegion, fcmToken, …) that iOS drops as collateral.
|
|
115
|
+
assertFalse(NotificationManager.shouldIncludeInData("fcm.notification"));
|
|
116
|
+
assertFalse(NotificationManager.shouldIncludeInData("fcm.foo"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test
|
|
120
|
+
public void shouldIncludeInData_fcmLookalike_returnsTrue() {
|
|
121
|
+
// These survive the Android filter. They would NOT survive the iOS
|
|
122
|
+
// filter, which is an accepted cross-platform divergence in favor of
|
|
123
|
+
// preserving realistic user data on Android. Note: `fcm_options` is a
|
|
124
|
+
// deliberate EXCEPTION to this rule — see the dedicated test
|
|
125
|
+
// shouldIncludeInData_fcmOptionsExactKey_returnsFalse below.
|
|
126
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcm"));
|
|
127
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcmlike"));
|
|
128
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcmRegion"));
|
|
129
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcmToken"));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
public void shouldIncludeInData_fcmOptionsExactKey_returnsFalse() {
|
|
134
|
+
// `fcm_options` is the Firebase HTTP v1 Message.fcm_options analytics
|
|
135
|
+
// label. On iOS it is caught by the bare `[key hasPrefix:@"fcm"]` filter
|
|
136
|
+
// in NotifeeCoreUtil.m:627-628, which has an inline `// fcm_options`
|
|
137
|
+
// comment documenting that the whole reason the iOS prefix is bare (not
|
|
138
|
+
// dotted) is specifically to drop this key. On Android we switched the
|
|
139
|
+
// prefix to `fcm.` (with dot) so realistic user keys like `fcmRegion`
|
|
140
|
+
// can survive — but that alone would leak `fcm_options`. This test
|
|
141
|
+
// guards the exact-match entry in EXCLUDED_DATA_KEYS that restores
|
|
142
|
+
// parity with iOS for the one Firebase-reserved bare-`fcm` key.
|
|
143
|
+
assertFalse(NotificationManager.shouldIncludeInData("fcm_options"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
public void shouldIncludeInData_exactKeyBlocklist_returnsFalse() {
|
|
148
|
+
assertFalse(NotificationManager.shouldIncludeInData("from"));
|
|
149
|
+
assertFalse(NotificationManager.shouldIncludeInData("collapse_key"));
|
|
150
|
+
assertFalse(NotificationManager.shouldIncludeInData("message_type"));
|
|
151
|
+
assertFalse(NotificationManager.shouldIncludeInData("message_id"));
|
|
152
|
+
assertFalse(NotificationManager.shouldIncludeInData("aps"));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
public void shouldIncludeInData_nullKey_returnsFalse() {
|
|
157
|
+
assertFalse(NotificationManager.shouldIncludeInData(null));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -------- extractDataFromExtras (bundle → bundle) --------
|
|
161
|
+
|
|
162
|
+
@Test
|
|
163
|
+
public void extractDataFromExtras_nullExtras_returnsEmptyBundle() {
|
|
164
|
+
Bundle result = NotificationManager.extractDataFromExtras(null);
|
|
165
|
+
assertNotNull(result);
|
|
166
|
+
assertEquals(0, result.size());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
public void extractDataFromExtras_emptyExtras_returnsEmptyBundle() {
|
|
171
|
+
Bundle result = NotificationManager.extractDataFromExtras(new Bundle());
|
|
172
|
+
assertNotNull(result);
|
|
173
|
+
assertEquals(0, result.size());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
public void extractDataFromExtras_mixedKeys_returnsOnlyCustomKeys() {
|
|
178
|
+
Bundle extras = new Bundle();
|
|
179
|
+
|
|
180
|
+
// Custom keys that should survive the filter.
|
|
181
|
+
extras.putString("event", "chat_msg");
|
|
182
|
+
extras.putString("userId", "42");
|
|
183
|
+
|
|
184
|
+
// System keys that must be dropped.
|
|
185
|
+
extras.putString("android.title", "Hello");
|
|
186
|
+
extras.putString("android.text", "Body");
|
|
187
|
+
extras.putString("google.sent_time", "123456");
|
|
188
|
+
extras.putString("gcm.notification.e", "1");
|
|
189
|
+
extras.putString("from", "12345");
|
|
190
|
+
extras.putString("collapse_key", "do_not_collapse");
|
|
191
|
+
extras.putString("message_id", "msg-abc");
|
|
192
|
+
extras.putString("notifee.notification", "internal");
|
|
193
|
+
extras.putString("fcm.foo", "reserved-dotted");
|
|
194
|
+
|
|
195
|
+
// Custom key matching the iOS-divergent survivor pattern: `fcmRegion`
|
|
196
|
+
// must appear in the result on Android, even though iOS would drop it.
|
|
197
|
+
extras.putString("fcmRegion", "eu-west-1");
|
|
198
|
+
|
|
199
|
+
// Non-String value to verify toString() coercion for the rare survivor.
|
|
200
|
+
extras.putInt("count", 5);
|
|
201
|
+
|
|
202
|
+
Bundle result = NotificationManager.extractDataFromExtras(extras);
|
|
203
|
+
assertNotNull(result);
|
|
204
|
+
|
|
205
|
+
Set<String> expected = new HashSet<>(Arrays.asList("event", "userId", "fcmRegion", "count"));
|
|
206
|
+
assertEquals(expected, result.keySet());
|
|
207
|
+
assertEquals("chat_msg", result.getString("event"));
|
|
208
|
+
assertEquals("42", result.getString("userId"));
|
|
209
|
+
assertEquals("eu-west-1", result.getString("fcmRegion"));
|
|
210
|
+
assertEquals("5", result.getString("count"));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Test
|
|
214
|
+
public void extractDataFromExtras_onlySystemKeys_returnsEmptyBundle() {
|
|
215
|
+
Bundle extras = new Bundle();
|
|
216
|
+
extras.putString("android.title", "Hello");
|
|
217
|
+
extras.putString("google.sent_time", "123");
|
|
218
|
+
extras.putString("gcm.notification.foo", "bar");
|
|
219
|
+
extras.putString("notifee.trigger", "internal");
|
|
220
|
+
extras.putString("from", "12345");
|
|
221
|
+
extras.putString("fcm.options", "reserved-dotted");
|
|
222
|
+
// Firebase analytics label — exact-match filtered for iOS parity,
|
|
223
|
+
// see shouldIncludeInData_fcmOptionsExactKey_returnsFalse.
|
|
224
|
+
extras.putString("fcm_options", "{\"analytics_label\":\"foo\"}");
|
|
225
|
+
|
|
226
|
+
Bundle result = NotificationManager.extractDataFromExtras(extras);
|
|
227
|
+
assertNotNull(result);
|
|
228
|
+
assertEquals(0, result.size());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Test
|
|
232
|
+
public void extractDataFromExtras_nullValueForCustomKey_isSkipped() {
|
|
233
|
+
Bundle extras = new Bundle();
|
|
234
|
+
extras.putString("event", "chat_msg");
|
|
235
|
+
extras.putString("nullable", null);
|
|
236
|
+
|
|
237
|
+
Bundle result = NotificationManager.extractDataFromExtras(extras);
|
|
238
|
+
assertNotNull(result);
|
|
239
|
+
assertEquals(1, result.size());
|
|
240
|
+
assertEquals("chat_msg", result.getString("event"));
|
|
241
|
+
}
|
|
242
|
+
}
|