react-native-notify-kit 9.5.0 → 9.7.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.
Files changed (25) hide show
  1. package/README.md +204 -2
  2. package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +378 -13
  3. package/android/src/main/java/app/notifee/core/AlarmPermissionBroadcastReceiver.java +17 -6
  4. package/android/src/main/java/app/notifee/core/InitProvider.java +109 -2
  5. package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +228 -17
  6. package/android/src/main/java/app/notifee/core/NotificationAlarmReceiver.java +16 -5
  7. package/android/src/main/java/app/notifee/core/NotificationManager.java +98 -0
  8. package/android/src/main/java/app/notifee/core/Preferences.java +9 -0
  9. package/android/src/main/java/app/notifee/core/RebootBroadcastReceiver.java +20 -5
  10. package/android/src/main/kotlin/io/invertase/notifee/HeadlessTask.kt +3 -2
  11. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +251 -4
  12. package/android/src/test/java/app/notifee/core/GetDisplayedNotificationsDataTest.java +242 -0
  13. package/android/src/test/java/app/notifee/core/InitProviderBootCheckTest.java +81 -0
  14. package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerHandleStaleTest.java +242 -0
  15. package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerSetAlarmClockTest.java +264 -0
  16. package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +199 -3
  17. package/android/src/test/java/io/invertase/notifee/HeadlessTaskConfigTest.kt +34 -0
  18. package/dist/types/Notification.d.ts +35 -4
  19. package/dist/types/Notification.js.map +1 -1
  20. package/dist/version.d.ts +1 -1
  21. package/dist/version.js +1 -1
  22. package/jest-mock.js +1 -0
  23. package/package.json +1 -1
  24. package/src/types/Notification.ts +35 -4
  25. package/src/version.ts +1 -1
@@ -2,16 +2,37 @@ package app.notifee.core.model;
2
2
 
3
3
  import static org.junit.Assert.assertEquals;
4
4
  import static org.junit.Assert.assertNull;
5
+ import static org.junit.Assert.assertTrue;
5
6
 
6
7
  import android.os.Bundle;
8
+ import java.util.Calendar;
9
+ import java.util.TimeZone;
10
+ import org.junit.After;
11
+ import org.junit.Assume;
7
12
  import org.junit.Before;
8
13
  import org.junit.Test;
14
+ import org.junit.runner.RunWith;
15
+ import org.robolectric.RobolectricTestRunner;
9
16
 
17
+ @RunWith(RobolectricTestRunner.class)
10
18
  public class TimestampTriggerModelTest {
19
+ private static final long ONE_MINUTE_MS = 60L * 1000L;
20
+ private static final long ONE_HOUR_MS = 60L * 60L * 1000L;
21
+ private static final long ONE_DAY_MS = 24L * ONE_HOUR_MS;
22
+
23
+ private static final int REPEAT_FREQUENCY_HOURLY = 0;
24
+ private static final int REPEAT_FREQUENCY_DAILY = 1;
25
+ private static final int REPEAT_FREQUENCY_WEEKLY = 2;
26
+
11
27
  private TimestampTriggerModel mTimestampTriggerModel = null;
28
+ private TimeZone mOriginalTimeZone;
29
+ private long mNow;
12
30
 
13
31
  @Before
14
32
  public void before() {
33
+ mOriginalTimeZone = TimeZone.getDefault();
34
+ mNow = System.currentTimeMillis();
35
+
15
36
  Bundle trigger = new Bundle();
16
37
  Bundle triggerComponents = new Bundle();
17
38
  triggerComponents.putInt("minute", 1);
@@ -20,16 +41,18 @@ public class TimestampTriggerModelTest {
20
41
  triggerComponents.putInt("month", 12);
21
42
  triggerComponents.putInt("weekday", 3);
22
43
  triggerComponents.putInt("weekdayOrdinal", 2);
23
-
24
- // The ISO 8601 week date of the year.
25
44
  triggerComponents.putInt("weekOfYear", 24);
26
- // The week number of the months.
27
45
  triggerComponents.putInt("weekOfMonth", 2);
28
46
 
29
47
  trigger.putBundle("components", triggerComponents);
30
48
  mTimestampTriggerModel = TimestampTriggerModel.fromBundle(trigger);
31
49
  }
32
50
 
51
+ @After
52
+ public void after() {
53
+ TimeZone.setDefault(mOriginalTimeZone);
54
+ }
55
+
33
56
  @Test
34
57
  public void testBundleValues() {
35
58
  assertEquals(
@@ -41,4 +64,177 @@ public class TimestampTriggerModelTest {
41
64
  assertEquals(
42
65
  "with no 'repeatFrequency', delay should be 0", 0, mTimestampTriggerModel.getDelay());
43
66
  }
67
+
68
+ // Regression tests for the DAILY/WEEKLY/HOURLY rescheduling cycle. Guard the fix for
69
+ // upstream invertase/notifee#839 (DAILY trigger fails to re-fire from day 2 onwards on
70
+ // Android) and #875 (DST-safe Calendar.add for repeat frequency) against future refactors
71
+ // of TimestampTriggerModel.setNextTimestamp().
72
+
73
+ private TimestampTriggerModel buildRepeatingTrigger(long timestamp, int repeatFrequency) {
74
+ Bundle trigger = new Bundle();
75
+ trigger.putLong("timestamp", timestamp);
76
+ trigger.putInt("repeatFrequency", repeatFrequency);
77
+ return TimestampTriggerModel.fromBundle(trigger);
78
+ }
79
+
80
+ @Test
81
+ public void setNextTimestamp_daily_advancesToTomorrowSameWallClock() {
82
+ long original = mNow - ONE_MINUTE_MS;
83
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_DAILY);
84
+
85
+ Calendar originalCal = Calendar.getInstance();
86
+ originalCal.setTimeInMillis(original);
87
+ int originalHour = originalCal.get(Calendar.HOUR_OF_DAY);
88
+ int originalMinute = originalCal.get(Calendar.MINUTE);
89
+ int originalSecond = originalCal.get(Calendar.SECOND);
90
+
91
+ trigger.setNextTimestamp();
92
+ long next = trigger.getTimestamp();
93
+
94
+ assertTrue("next timestamp must be in the future", next > mNow);
95
+ assertTrue("next timestamp must be within 25h", next < mNow + 25L * ONE_HOUR_MS);
96
+
97
+ Calendar nextCal = Calendar.getInstance();
98
+ nextCal.setTimeInMillis(next);
99
+ assertEquals("wall-clock hour preserved", originalHour, nextCal.get(Calendar.HOUR_OF_DAY));
100
+ assertEquals("wall-clock minute preserved", originalMinute, nextCal.get(Calendar.MINUTE));
101
+ assertEquals("wall-clock second preserved", originalSecond, nextCal.get(Calendar.SECOND));
102
+ }
103
+
104
+ @Test
105
+ public void setNextTimestamp_daily_multipleMissedFires_skipsToFuture() {
106
+ // Offset by 1 minute so the original does not land exactly on a multiple of 24h from mNow.
107
+ // Otherwise, with a deterministic clock (e.g. Robolectric), the while-loop's `<` comparison
108
+ // exits precisely at `cal == now` and next equals mNow, which is not strictly in the future.
109
+ long original = mNow - 3L * ONE_DAY_MS - ONE_MINUTE_MS;
110
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_DAILY);
111
+
112
+ trigger.setNextTimestamp();
113
+ long next = trigger.getTimestamp();
114
+
115
+ assertTrue("next timestamp must be in the future after 3 missed fires", next > mNow);
116
+ assertTrue("next timestamp must not be more than 25h ahead", next < mNow + 25L * ONE_HOUR_MS);
117
+ }
118
+
119
+ @Test
120
+ public void setNextTimestamp_weekly_advancesOneWeek() {
121
+ long original = mNow - ONE_MINUTE_MS;
122
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_WEEKLY);
123
+
124
+ trigger.setNextTimestamp();
125
+ long next = trigger.getTimestamp();
126
+
127
+ long lower = mNow + 6L * ONE_DAY_MS + 23L * ONE_HOUR_MS;
128
+ long upper = mNow + 7L * ONE_DAY_MS + ONE_HOUR_MS;
129
+ assertTrue("weekly next timestamp must be >= now + 6d23h", next >= lower);
130
+ assertTrue("weekly next timestamp must be <= now + 7d1h", next <= upper);
131
+ }
132
+
133
+ @Test
134
+ public void setNextTimestamp_hourly_advancesOneHour() {
135
+ long original = mNow - ONE_MINUTE_MS;
136
+ TimestampTriggerModel trigger = buildRepeatingTrigger(original, REPEAT_FREQUENCY_HOURLY);
137
+
138
+ trigger.setNextTimestamp();
139
+ long next = trigger.getTimestamp();
140
+
141
+ long lower = mNow + 59L * ONE_MINUTE_MS;
142
+ long upper = mNow + 61L * ONE_MINUTE_MS;
143
+ assertTrue("hourly next timestamp must be >= now + 59m", next >= lower);
144
+ assertTrue("hourly next timestamp must be <= now + 61m", next <= upper);
145
+ }
146
+
147
+ @Test
148
+ public void setNextTimestamp_dailyAcrossDstSpringForward() {
149
+ // Europe/Rome spring-forward 2026: 2026-03-29 02:00 local jumps to 03:00 local. The 29
150
+ // March 2026 calendar day is 23 hours long in Europe/Rome. Using 04:30 (NOT 01:30) as
151
+ // the wall-clock: 04:30 on 2026-03-28 is CET (UTC+1), while 04:30 on 2026-03-29 is CEST
152
+ // (UTC+2). Calendar.add(DAY_OF_MONTH, 1) preserves the local 04:30 wall-clock across the
153
+ // boundary; a hypothetical refactor to +86_400_000 ms fixed arithmetic would drift the
154
+ // wall-clock to 05:30 CEST on the day after the crossing. Using 01:30 would NOT
155
+ // discriminate because 01:30 sits in the same UTC offset on both sides of the transition.
156
+ TimeZone rome = TimeZone.getTimeZone("Europe/Rome");
157
+ TimeZone.setDefault(rome);
158
+
159
+ Calendar start = Calendar.getInstance(rome);
160
+ start.clear();
161
+ start.set(2026, Calendar.MARCH, 28, 4, 30, 0);
162
+ long originalTimestamp = start.getTimeInMillis();
163
+
164
+ // Assume the current wall-clock is in the post-spring-forward / pre-fall-back window for
165
+ // 2026. Outside this window the while-loop inside setNextTimestamp either does not run
166
+ // (future-dated start) or crosses an even number of DST boundaries whose effects cancel
167
+ // for fixed-ms arithmetic, making the assertion non-discriminative.
168
+ Calendar windowStart = Calendar.getInstance(rome);
169
+ windowStart.clear();
170
+ windowStart.set(2026, Calendar.MARCH, 30, 0, 0, 0);
171
+ Calendar windowEnd = Calendar.getInstance(rome);
172
+ windowEnd.clear();
173
+ windowEnd.set(2026, Calendar.OCTOBER, 24, 23, 59, 59);
174
+ long now = System.currentTimeMillis();
175
+ Assume.assumeTrue(
176
+ "spring-forward discrimination requires current time in [2026-03-30, 2026-10-24]"
177
+ + " Europe/Rome",
178
+ now >= windowStart.getTimeInMillis() && now <= windowEnd.getTimeInMillis());
179
+
180
+ TimestampTriggerModel trigger =
181
+ buildRepeatingTrigger(originalTimestamp, REPEAT_FREQUENCY_DAILY);
182
+ trigger.setNextTimestamp();
183
+ long next = trigger.getTimestamp();
184
+
185
+ Calendar nextCal = Calendar.getInstance(rome);
186
+ nextCal.setTimeInMillis(next);
187
+ assertTrue("next timestamp must be >= now", next >= now);
188
+ assertEquals(
189
+ "wall-clock hour must remain 4 after crossing spring-forward",
190
+ 4,
191
+ nextCal.get(Calendar.HOUR_OF_DAY));
192
+ assertEquals("wall-clock minute must remain 30", 30, nextCal.get(Calendar.MINUTE));
193
+ assertEquals("wall-clock second must remain 0", 0, nextCal.get(Calendar.SECOND));
194
+ }
195
+
196
+ @Test
197
+ public void setNextTimestamp_dailyAcrossDstFallBack() {
198
+ // Europe/Rome fall-back 2025: 2025-10-26 03:00 local reverts to 02:00 local. The 26
199
+ // October 2025 calendar day is 25 hours long. Starting at 2025-10-25 04:30 CEST (UTC+2)
200
+ // — the day before the fall-back — the local 04:30 wall-clock on 2025-10-26 is CET
201
+ // (UTC+1). Calendar.add preserves the 04:30 local wall-clock; +86_400_000 ms would drift
202
+ // it to 03:30 CET after the crossing.
203
+ TimeZone rome = TimeZone.getTimeZone("Europe/Rome");
204
+ TimeZone.setDefault(rome);
205
+
206
+ Calendar start = Calendar.getInstance(rome);
207
+ start.clear();
208
+ start.set(2025, Calendar.OCTOBER, 25, 4, 30, 0);
209
+ long originalTimestamp = start.getTimeInMillis();
210
+
211
+ // Assume the current wall-clock is in the post-fall-back-2025 / pre-spring-forward-2026
212
+ // window, so the setNextTimestamp loop crosses exactly one DST boundary (the fall-back)
213
+ // and the test remains discriminative against fixed-ms arithmetic.
214
+ Calendar windowStart = Calendar.getInstance(rome);
215
+ windowStart.clear();
216
+ windowStart.set(2025, Calendar.OCTOBER, 27, 0, 0, 0);
217
+ Calendar windowEnd = Calendar.getInstance(rome);
218
+ windowEnd.clear();
219
+ windowEnd.set(2026, Calendar.MARCH, 28, 23, 59, 59);
220
+ long now = System.currentTimeMillis();
221
+ Assume.assumeTrue(
222
+ "fall-back discrimination requires current time in [2025-10-27, 2026-03-28] Europe/Rome",
223
+ now >= windowStart.getTimeInMillis() && now <= windowEnd.getTimeInMillis());
224
+
225
+ TimestampTriggerModel trigger =
226
+ buildRepeatingTrigger(originalTimestamp, REPEAT_FREQUENCY_DAILY);
227
+ trigger.setNextTimestamp();
228
+ long next = trigger.getTimestamp();
229
+
230
+ Calendar nextCal = Calendar.getInstance(rome);
231
+ nextCal.setTimeInMillis(next);
232
+ assertTrue("next timestamp must be >= now", next >= now);
233
+ assertEquals(
234
+ "wall-clock hour must remain 4 after crossing fall-back",
235
+ 4,
236
+ nextCal.get(Calendar.HOUR_OF_DAY));
237
+ assertEquals("wall-clock minute must remain 30", 30, nextCal.get(Calendar.MINUTE));
238
+ assertEquals("wall-clock second must remain 0", 0, nextCal.get(Calendar.SECOND));
239
+ }
44
240
  }
@@ -0,0 +1,34 @@
1
+ package io.invertase.notifee
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+ import org.junit.Test
5
+ import org.mockito.ArgumentMatchers.anyInt
6
+ import org.mockito.ArgumentMatchers.anyString
7
+ import org.mockito.Mockito.mock
8
+ import org.mockito.Mockito.never
9
+ import org.mockito.Mockito.times
10
+ import org.mockito.Mockito.verify
11
+ import org.mockito.Mockito.`when`
12
+
13
+ class HeadlessTaskConfigTest {
14
+
15
+ // Regression guard for invertase/notifee#266: the init block must copy the caller's
16
+ // WritableMap before mutating it, otherwise a later .copy() on the already-consumed map crashes.
17
+ @Test
18
+ fun taskConfig_copiesParamsBeforeMutating_keepsCallerMapPristine() {
19
+ val original = mock(WritableMap::class.java)
20
+ val copy = mock(WritableMap::class.java)
21
+ `when`(original.copy()).thenReturn(copy)
22
+
23
+ HeadlessTask.TaskConfig(
24
+ "NotifeeHeadlessJS",
25
+ 60_000L,
26
+ original,
27
+ null,
28
+ )
29
+
30
+ verify(original, never()).putInt(anyString(), anyInt())
31
+ verify(original, times(1)).copy()
32
+ verify(copy, times(1)).putInt(anyString(), anyInt())
33
+ }
34
+ }
@@ -32,10 +32,41 @@ export interface Notification {
32
32
  */
33
33
  body?: string | undefined;
34
34
  /**
35
- * Additional data to store on the notification.
36
- *
37
- * Data can be used to provide additional context to your notification which can be retrieved
38
- * at a later point in time (e.g. via an event).
35
+ * Additional data to associate with the notification.
36
+ *
37
+ * On Android, when populated by `getDisplayedNotifications()`, the `data`
38
+ * field reflects custom keys present in the underlying `Notification.extras`,
39
+ * with the following keys filtered out: prefixes `android.`, `google.`,
40
+ * `gcm.`, `fcm.` (with trailing dot — `fcmRegion` survives), and `notifee`
41
+ * (no trailing dot — library's reserved namespace, `notifeeFoo` is also
42
+ * filtered), plus exact keys `from`, `collapse_key`, `message_type`,
43
+ * `message_id`, `aps`, `fcm_options`.
44
+ *
45
+ * **Important FCM platform limitation on Android**: when the FCM Android SDK
46
+ * auto-displays a `notification`+`data` push while the app is in background
47
+ * or killed, custom `data` fields are placed only on the tap-action
48
+ * `PendingIntent` and never on `Notification.extras`. This is FCM's design
49
+ * (see `CommonNotificationBuilder.createContentIntent` in the firebase-android-sdk
50
+ * source) and cannot be worked around from any library — the PendingIntent's
51
+ * extras are sealed by Android's security model. Tracked in
52
+ * firebase-android-sdk#2639 (open since 2021).
53
+ *
54
+ * The `data` field DOES surface custom keys for: notifications created via
55
+ * `notifee.displayNotification({ data })`, FCM data-only messages handled
56
+ * in `onMessageReceived` followed by an explicit `displayNotification()`,
57
+ * custom `FirebaseMessagingService.handleIntent` overrides that inject
58
+ * extras before display, and notifications posted by other libraries that
59
+ * call `NotificationCompat.Builder.addExtras(bundle)`.
60
+ *
61
+ * Recommended pattern for full control over FCM push notifications on Android:
62
+ * send FCM data-only messages (no `notification` field server-side), handle
63
+ * them in `onMessageReceived`, and render the notification via
64
+ * `notifee.displayNotification()`.
65
+ *
66
+ * Note: the `fcm` filter diverges from iOS, which uses a broader bare-`fcm*`
67
+ * prefix filter (deliberately, to catch `fcm_options`). On Android, only
68
+ * `fcm.` (with dot) and the exact key `fcm_options` are filtered — custom
69
+ * keys like `fcmRegion` are preserved on Android but filtered on iOS.
39
70
  */
40
71
  data?: {
41
72
  [key: string]: string | object | number;
@@ -1 +1 @@
1
- {"version":3,"file":"Notification.js","sourceRoot":"","sources":["../../src/types/Notification.ts"],"names":[],"mappings":"AAAA;;GAEG;AA6SH;;;;;GAKG;AACH,MAAM,CAAN,IAAY,SA2EX;AA3ED,WAAY,SAAS;IACnB;;;;;OAKG;IACH,gDAAY,CAAA;IAEZ;;;;;;;OAOG;IACH,mDAAa,CAAA;IAEb;;;;;;OAMG;IACH,2CAAS,CAAA;IAET;;OAEG;IACH,yDAAgB,CAAA;IAEhB;;;;;;OAMG;IACH,mDAAa,CAAA;IAEb;;;;;OAKG;IACH,uDAAe,CAAA;IAEf;;;;OAIG;IACH,+DAAmB,CAAA;IAEnB;;;;OAIG;IACH,2EAAyB,CAAA;IAEzB;;OAEG;IACH,yFAAgC,CAAA;IAEhC;;;;OAIG;IACH,iEAAoB,CAAA;AACtB,CAAC,EA3EW,SAAS,KAAT,SAAS,QA2EpB;AA+ED;;;;;;GAMG;AACH,MAAM,CAAN,IAAY,mBA0BX;AA1BD,WAAY,mBAAmB;IAC7B;;;;;;;OAOG;IACH,kFAAmB,CAAA;IAEnB;;OAEG;IACH,iEAAU,CAAA;IAEV;;OAEG;IACH,yEAAc,CAAA;IAEd;;;OAGG;IACH,2EAAe,CAAA;AACjB,CAAC,EA1BW,mBAAmB,KAAnB,mBAAmB,QA0B9B"}
1
+ {"version":3,"file":"Notification.js","sourceRoot":"","sources":["../../src/types/Notification.ts"],"names":[],"mappings":"AAAA;;GAEG;AA4UH;;;;;GAKG;AACH,MAAM,CAAN,IAAY,SA2EX;AA3ED,WAAY,SAAS;IACnB;;;;;OAKG;IACH,gDAAY,CAAA;IAEZ;;;;;;;OAOG;IACH,mDAAa,CAAA;IAEb;;;;;;OAMG;IACH,2CAAS,CAAA;IAET;;OAEG;IACH,yDAAgB,CAAA;IAEhB;;;;;;OAMG;IACH,mDAAa,CAAA;IAEb;;;;;OAKG;IACH,uDAAe,CAAA;IAEf;;;;OAIG;IACH,+DAAmB,CAAA;IAEnB;;;;OAIG;IACH,2EAAyB,CAAA;IAEzB;;OAEG;IACH,yFAAgC,CAAA;IAEhC;;;;OAIG;IACH,iEAAoB,CAAA;AACtB,CAAC,EA3EW,SAAS,KAAT,SAAS,QA2EpB;AA+ED;;;;;;GAMG;AACH,MAAM,CAAN,IAAY,mBA0BX;AA1BD,WAAY,mBAAmB;IAC7B;;;;;;;OAOG;IACH,kFAAmB,CAAA;IAEnB;;OAEG;IACH,iEAAU,CAAA;IAEV;;OAEG;IACH,yEAAc,CAAA;IAEd;;;OAGG;IACH,2EAAe,CAAA;AACjB,CAAC,EA1BW,mBAAmB,KAAnB,mBAAmB,QA0B9B"}
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "9.5.0";
1
+ export declare const version = "9.7.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by genversion.
2
- export const version = '9.5.0';
2
+ export const version = '9.7.0';
3
3
  //# sourceMappingURL=version.js.map
package/jest-mock.js CHANGED
@@ -26,6 +26,7 @@ export const testNotification = {
26
26
  id: 'test-id',
27
27
  title: 'test-title',
28
28
  body: 'test-body',
29
+ data: { event: 'test_event', userId: '42' },
29
30
  android: {
30
31
  channelId: 'default',
31
32
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-notify-kit",
3
- "version": "9.5.0",
3
+ "version": "9.7.0",
4
4
  "author": "Marco Crupi",
5
5
  "description": "Maintained Notifee-compatible fork for React Native — advanced local notifications for Android & iOS.",
6
6
  "main": "dist/index.js",
@@ -46,10 +46,41 @@ export interface Notification {
46
46
  body?: string | undefined;
47
47
 
48
48
  /**
49
- * Additional data to store on the notification.
50
- *
51
- * Data can be used to provide additional context to your notification which can be retrieved
52
- * at a later point in time (e.g. via an event).
49
+ * Additional data to associate with the notification.
50
+ *
51
+ * On Android, when populated by `getDisplayedNotifications()`, the `data`
52
+ * field reflects custom keys present in the underlying `Notification.extras`,
53
+ * with the following keys filtered out: prefixes `android.`, `google.`,
54
+ * `gcm.`, `fcm.` (with trailing dot — `fcmRegion` survives), and `notifee`
55
+ * (no trailing dot — library's reserved namespace, `notifeeFoo` is also
56
+ * filtered), plus exact keys `from`, `collapse_key`, `message_type`,
57
+ * `message_id`, `aps`, `fcm_options`.
58
+ *
59
+ * **Important FCM platform limitation on Android**: when the FCM Android SDK
60
+ * auto-displays a `notification`+`data` push while the app is in background
61
+ * or killed, custom `data` fields are placed only on the tap-action
62
+ * `PendingIntent` and never on `Notification.extras`. This is FCM's design
63
+ * (see `CommonNotificationBuilder.createContentIntent` in the firebase-android-sdk
64
+ * source) and cannot be worked around from any library — the PendingIntent's
65
+ * extras are sealed by Android's security model. Tracked in
66
+ * firebase-android-sdk#2639 (open since 2021).
67
+ *
68
+ * The `data` field DOES surface custom keys for: notifications created via
69
+ * `notifee.displayNotification({ data })`, FCM data-only messages handled
70
+ * in `onMessageReceived` followed by an explicit `displayNotification()`,
71
+ * custom `FirebaseMessagingService.handleIntent` overrides that inject
72
+ * extras before display, and notifications posted by other libraries that
73
+ * call `NotificationCompat.Builder.addExtras(bundle)`.
74
+ *
75
+ * Recommended pattern for full control over FCM push notifications on Android:
76
+ * send FCM data-only messages (no `notification` field server-side), handle
77
+ * them in `onMessageReceived`, and render the notification via
78
+ * `notifee.displayNotification()`.
79
+ *
80
+ * Note: the `fcm` filter diverges from iOS, which uses a broader bare-`fcm*`
81
+ * prefix filter (deliberately, to catch `fcm_options`). On Android, only
82
+ * `fcm.` (with dot) and the exact key `fcm_options` are filtered — custom
83
+ * keys like `fcmRegion` are preserved on Android but filtered on iOS.
53
84
  */
54
85
  data?: { [key: string]: string | object | number };
55
86
 
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '9.5.0';
2
+ export const version = '9.7.0';