react-native-notify-kit 9.2.1 → 9.4.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 (50) hide show
  1. package/README.md +85 -7
  2. package/android/build.gradle +6 -5
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/baseline-prof.txt +382 -0
  5. package/android/src/main/java/app/notifee/core/ForegroundService.java +269 -80
  6. package/android/src/main/java/app/notifee/core/InitProvider.java +48 -5
  7. package/android/src/main/java/app/notifee/core/Logger.java +5 -0
  8. package/android/src/main/java/app/notifee/core/NotificationManager.java +217 -150
  9. package/android/src/main/java/app/notifee/core/NotificationPendingIntent.java +10 -0
  10. package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
  11. package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +17 -1
  12. package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +30 -0
  13. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +182 -0
  14. package/android/src/test/java/app/notifee/core/NotificationPendingIntentTest.java +108 -0
  15. package/dist/NotifeeApiModule.d.ts +1 -0
  16. package/dist/NotifeeApiModule.js +6 -0
  17. package/dist/NotifeeApiModule.js.map +1 -1
  18. package/dist/specs/NativeNotifeeModule.d.ts +1 -0
  19. package/dist/specs/NativeNotifeeModule.js.map +1 -1
  20. package/dist/types/Module.d.ts +19 -0
  21. package/dist/types/NotificationAndroid.d.ts +66 -4
  22. package/dist/types/NotificationAndroid.js +43 -0
  23. package/dist/types/NotificationAndroid.js.map +1 -1
  24. package/dist/utils/validate.d.ts +1 -0
  25. package/dist/utils/validate.js +10 -4
  26. package/dist/utils/validate.js.map +1 -1
  27. package/dist/validators/validateAndroidChannel.js +3 -3
  28. package/dist/validators/validateAndroidChannel.js.map +1 -1
  29. package/dist/validators/validateAndroidNotification.js +58 -14
  30. package/dist/validators/validateAndroidNotification.js.map +1 -1
  31. package/dist/validators/validateIOSPermissions.js +1 -1
  32. package/dist/validators/validateIOSPermissions.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +5 -1
  36. package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +15 -15
  37. package/ios/NotifeeCore/NotifeeCore.m +41 -14
  38. package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +32 -18
  39. package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
  40. package/ios/RNNotifee/NotifeeApiModule.mm +33 -11
  41. package/package.json +1 -1
  42. package/src/NotifeeApiModule.ts +7 -0
  43. package/src/specs/NativeNotifeeModule.ts +1 -0
  44. package/src/types/Module.ts +20 -0
  45. package/src/types/NotificationAndroid.ts +70 -4
  46. package/src/utils/validate.ts +12 -4
  47. package/src/validators/validateAndroidChannel.ts +3 -3
  48. package/src/validators/validateAndroidNotification.ts +64 -11
  49. package/src/validators/validateIOSPermissions.ts +1 -1
  50. package/src/version.ts +1 -1
@@ -28,6 +28,9 @@ static NSInteger kReactNativeNotifeeNotificationTypeTrigger = 2;
28
28
  static NSInteger kReactNativeNotifeeNotificationTypeAll = 0;
29
29
 
30
30
  @implementation NotifeeApiModule {
31
+ // Guarded by @synchronized(self): didReceiveNotifeeCoreEvent: may be called
32
+ // from arbitrary threads (UNUserNotificationCenter callbacks), while
33
+ // startObserving/stopObserving/sendNotifeeCoreEvent: run on the main thread.
31
34
  bool hasListeners;
32
35
  NSMutableArray *pendingCoreEvents;
33
36
  }
@@ -53,15 +56,21 @@ RCT_EXPORT_MODULE();
53
56
  }
54
57
 
55
58
  - (void)startObserving {
56
- hasListeners = YES;
57
- for (NSDictionary *eventBody in pendingCoreEvents) {
59
+ NSArray *eventsToFlush;
60
+ @synchronized(self) {
61
+ hasListeners = YES;
62
+ eventsToFlush = [pendingCoreEvents copy];
63
+ [pendingCoreEvents removeAllObjects];
64
+ }
65
+ for (NSDictionary *eventBody in eventsToFlush) {
58
66
  [self sendNotifeeCoreEvent:eventBody];
59
67
  }
60
- [pendingCoreEvents removeAllObjects];
61
68
  }
62
69
 
63
70
  - (void)stopObserving {
64
- hasListeners = NO;
71
+ @synchronized(self) {
72
+ hasListeners = NO;
73
+ }
65
74
  }
66
75
 
67
76
  + (BOOL)requiresMainQueueSetup {
@@ -80,18 +89,25 @@ RCT_EXPORT_MODULE();
80
89
  #pragma mark - Events
81
90
 
82
91
  - (void)didReceiveNotifeeCoreEvent:(NSDictionary *_Nonnull)event {
83
- if (hasListeners) {
92
+ BOOL shouldSend;
93
+ @synchronized(self) {
94
+ shouldSend = hasListeners;
95
+ if (!shouldSend) {
96
+ [pendingCoreEvents addObject:event];
97
+ }
98
+ }
99
+ if (shouldSend) {
84
100
  [self sendNotifeeCoreEvent:event];
85
- } else {
86
- [pendingCoreEvents addObject:event];
87
101
  }
88
102
  }
89
103
 
90
104
  - (void)sendNotifeeCoreEvent:(NSDictionary *_Nonnull)eventBody {
91
105
  dispatch_async(dispatch_get_main_queue(), ^{
92
- if (!hasListeners) {
93
- [pendingCoreEvents addObject:eventBody];
94
- return;
106
+ @synchronized(self) {
107
+ if (!hasListeners) {
108
+ [pendingCoreEvents addObject:eventBody];
109
+ return;
110
+ }
95
111
  }
96
112
  // Routing foreground vs background is based on UIApplication state.
97
113
  // iOS delivers didReceiveNotificationResponse: while the app is in
@@ -107,7 +123,8 @@ RCT_EXPORT_MODULE();
107
123
  // API-triggered events require an interactive JS context. If this
108
124
  // becomes a real-world problem, the routing should be split by event
109
125
  // type rather than by state alone.
110
- BOOL isBackground = RCTRunningInAppExtension() ||
126
+ BOOL isBackground =
127
+ RCTRunningInAppExtension() ||
111
128
  [UIApplication sharedApplication].applicationState != UIApplicationStateActive;
112
129
  if (isBackground) {
113
130
  [self sendEventWithName:kReactNativeNotifeeNotificationBackgroundEvent body:eventBody];
@@ -417,6 +434,11 @@ RCT_EXPORT_MODULE();
417
434
  resolve(nil);
418
435
  }
419
436
 
437
+ - (void)prewarmForegroundService:(RCTPromiseResolveBlock)resolve
438
+ reject:(RCTPromiseRejectBlock)reject {
439
+ resolve(nil);
440
+ }
441
+
420
442
  - (void)hideNotificationDrawer {
421
443
  // Android-only, no-op on iOS
422
444
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-notify-kit",
3
- "version": "9.2.1",
3
+ "version": "9.4.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",
@@ -826,6 +826,13 @@ export default class NotifeeApiModule extends NotifeeNativeModule implements Mod
826
826
  return this.native.stopForegroundService();
827
827
  };
828
828
 
829
+ public prewarmForegroundService = (): Promise<void> => {
830
+ if (!isAndroid) {
831
+ return Promise.resolve();
832
+ }
833
+ return this.native.prewarmForegroundService();
834
+ };
835
+
829
836
  public hideNotificationDrawer = (): void => {
830
837
  if (!isAndroid) {
831
838
  return;
@@ -49,6 +49,7 @@ export interface Spec extends TurboModule {
49
49
  getPowerManagerInfo(): Promise<Object>;
50
50
  openPowerManagerSettings(): Promise<void>;
51
51
  stopForegroundService(): Promise<void>;
52
+ prewarmForegroundService(): Promise<void>;
52
53
  hideNotificationDrawer(): void;
53
54
  addListener(eventName: string): void;
54
55
  removeListeners(count: number): void;
@@ -419,6 +419,26 @@ export interface Module {
419
419
  */
420
420
  stopForegroundService(): Promise<void>;
421
421
 
422
+ /**
423
+ * Pre-warms the foreground service notification path by eagerly loading critical
424
+ * classes and warming the `INotificationManager` Binder proxy on a background thread.
425
+ *
426
+ * Most apps do **not** need this method — the library automatically performs warmup
427
+ * at app startup via `InitProvider`. This API is an escape hatch for edge cases:
428
+ *
429
+ * - Apps where the library is lazy-loaded or code-split
430
+ * - Apps that want to defer warmup to after the splash screen
431
+ * - Low-end devices where the `InitProvider` warmup hasn't finished by the time
432
+ * the first notification is displayed
433
+ *
434
+ * Safe to call multiple times (idempotent). On iOS this resolves immediately as a no-op.
435
+ *
436
+ * See the "Advanced / Troubleshooting" section in the README for more details.
437
+ *
438
+ * @platform android
439
+ */
440
+ prewarmForegroundService(): Promise<void>;
441
+
422
442
  /**
423
443
  * Request specific notification permissions for your application on the current device.
424
444
  *
@@ -262,14 +262,18 @@ export interface NotificationAndroid {
262
262
  onlyAlertOnce?: boolean;
263
263
 
264
264
  /**
265
- * By default notifications have no behaviour when a user presses them. The
266
- * `pressAction` property allows you to set what happens when a user presses
267
- * the notification.
265
+ * Controls what happens when a user presses the notification.
266
+ *
267
+ * Defaults to `{ id: 'default', launchActivity: 'default' }` when omitted,
268
+ * which opens the app's main activity on tap.
269
+ *
270
+ * Pass `null` explicitly to create a non-tappable notification (the notification
271
+ * will display but tapping it will do nothing).
268
272
  *
269
273
  * View the [Interaction](/react-native/android/interaction) documentation to learn
270
274
  * more.
271
275
  */
272
- pressAction?: NotificationPressAction;
276
+ pressAction?: NotificationPressAction | null;
273
277
 
274
278
  /**
275
279
  * The `fullScreenAction` property allows you to show a custom UI
@@ -288,6 +292,23 @@ export interface NotificationAndroid {
288
292
  */
289
293
  foregroundServiceTypes?: AndroidForegroundServiceType[];
290
294
 
295
+ /**
296
+ * Controls the display behavior of the foreground service notification on Android 12+.
297
+ *
298
+ * When `asForegroundService` is `true` and this property is not set, it defaults to
299
+ * `AndroidForegroundServiceBehavior.IMMEDIATE`, which eliminates the up to 10-second
300
+ * display delay that Android 12+ imposes on foreground service notifications.
301
+ *
302
+ * Set to `AndroidForegroundServiceBehavior.DEFERRED` to restore the platform default
303
+ * deferred behavior, or `AndroidForegroundServiceBehavior.DEFAULT` for the framework-neutral
304
+ * default.
305
+ *
306
+ * Ignored when `asForegroundService` is `false` or not set.
307
+ *
308
+ * @platform android API 31+
309
+ */
310
+ foregroundServiceBehavior?: AndroidForegroundServiceBehavior;
311
+
291
312
  /**
292
313
  * Set a notification importance for devices without channel support.
293
314
  *
@@ -1476,3 +1497,48 @@ export enum AndroidForegroundServiceType {
1476
1497
  FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED = 1024,
1477
1498
  FOREGROUND_SERVICE_TYPE_MANIFEST = -1,
1478
1499
  }
1500
+
1501
+ /**
1502
+ * Controls the display behavior of foreground service notifications on Android 12+.
1503
+ *
1504
+ * Values are aligned with androidx.core.app.NotificationCompat constants:
1505
+ * DEFAULT = NotificationCompat.FOREGROUND_SERVICE_DEFAULT (0)
1506
+ * IMMEDIATE = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE (1)
1507
+ * DEFERRED = NotificationCompat.FOREGROUND_SERVICE_DEFERRED (2)
1508
+ *
1509
+ * Do not change these numeric values without updating the native getter in
1510
+ * NotificationAndroidModel.java.
1511
+ *
1512
+ * @platform android
1513
+ */
1514
+ export enum AndroidForegroundServiceBehavior {
1515
+ /**
1516
+ * Framework default. Equivalent to not calling `setForegroundServiceBehavior()` at all.
1517
+ * On Android 12+, the system may defer display of the foreground service notification
1518
+ * by up to 10 seconds unless an exemption applies.
1519
+ *
1520
+ * Corresponds to `NotificationCompat.FOREGROUND_SERVICE_DEFAULT`.
1521
+ */
1522
+ DEFAULT = 0,
1523
+
1524
+ /**
1525
+ * The foreground service notification is shown immediately after the service starts.
1526
+ * Eliminates the 10-second display delay imposed by Android 12+ on foreground service
1527
+ * notifications.
1528
+ *
1529
+ * This is the library default when `asForegroundService` is `true` and
1530
+ * `foregroundServiceBehavior` is not explicitly set.
1531
+ *
1532
+ * Corresponds to `NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE`.
1533
+ */
1534
+ IMMEDIATE = 1,
1535
+
1536
+ /**
1537
+ * The foreground service notification display may be deferred by up to 10 seconds by the
1538
+ * system. This matches the default Android 12+ behavior and the (unintended) behavior of
1539
+ * upstream `@notifee/react-native`.
1540
+ *
1541
+ * Corresponds to `NotificationCompat.FOREGROUND_SERVICE_DEFERRED`.
1542
+ */
1543
+ DEFERRED = 2,
1544
+ }
@@ -82,9 +82,17 @@ export function isValidUrl(url: string): boolean {
82
82
  }
83
83
 
84
84
  export function isValidEnum(value: any, enumType: Record<string, any>): boolean {
85
- if (!Object.values(enumType).includes(value)) {
86
- return false;
87
- }
85
+ // Filter out reverse-mapped string keys that TypeScript adds to numeric enums.
86
+ // In a numeric enum { A=0, B=1 }, Object.values() returns [0, 1, "A", "B"].
87
+ // The reverse-mapped string "A" satisfies typeof enumType["A"] === 'number',
88
+ // whereas real string enum values (e.g., TimeUnit.SECONDS = 'SECONDS')
89
+ // satisfy typeof enumType["SECONDS"] === 'string', so they are kept.
90
+ const values = Object.values(enumType).filter(
91
+ v => typeof v !== 'string' || typeof enumType[v] !== 'number',
92
+ );
93
+ return values.includes(value);
94
+ }
88
95
 
89
- return true;
96
+ export function getNumericEnumValues(enumType: Record<string, any>): number[] {
97
+ return Object.values(enumType).filter((v): v is number => typeof v === 'number');
90
98
  }
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) 2016-present Invertase Limited
3
3
  */
4
4
 
5
- import { objectHasProperty, isArray, isBoolean, isObject, isString } from '../utils';
5
+ import { objectHasProperty, isArray, isBoolean, isObject, isString, isValidEnum } from '../utils';
6
6
  import { isValidColor, isValidVibratePattern } from './validate';
7
7
 
8
8
  import { AndroidImportance } from '../types/NotificationAndroid';
@@ -121,7 +121,7 @@ export default function validateAndroidChannel(channel: AndroidChannel): Android
121
121
  * importance
122
122
  */
123
123
  if (objectHasProperty(channel, 'importance') && channel.importance !== undefined) {
124
- if (!Object.values(AndroidImportance).includes(channel.importance)) {
124
+ if (!isValidEnum(channel.importance, AndroidImportance)) {
125
125
  throw new Error("'channel.importance' expected an Importance value.");
126
126
  }
127
127
 
@@ -149,7 +149,7 @@ export default function validateAndroidChannel(channel: AndroidChannel): Android
149
149
  * visibility
150
150
  */
151
151
  if (objectHasProperty(channel, 'visibility') && channel.visibility !== undefined) {
152
- if (!Object.values(AndroidVisibility).includes(channel.visibility)) {
152
+ if (!isValidEnum(channel.visibility, AndroidVisibility)) {
153
153
  throw new Error("'channel.visibility' expected visibility to be an AndroidVisibility value.");
154
154
  }
155
155
 
@@ -13,6 +13,8 @@ import {
13
13
  isString,
14
14
  isUndefined,
15
15
  isIOS,
16
+ isValidEnum,
17
+ getNumericEnumValues,
16
18
  } from '../utils';
17
19
 
18
20
  import { AndroidImportance } from '../types/NotificationAndroid';
@@ -20,6 +22,7 @@ import {
20
22
  AndroidBadgeIconType,
21
23
  AndroidCategory,
22
24
  AndroidDefaults,
25
+ AndroidForegroundServiceBehavior,
23
26
  AndroidForegroundServiceType,
24
27
  AndroidFlags,
25
28
  AndroidGroupAlertBehavior,
@@ -116,6 +119,22 @@ export default function validateAndroidNotification(
116
119
  out.asForegroundService = android.asForegroundService;
117
120
  }
118
121
 
122
+ /**
123
+ * foregroundServiceBehavior
124
+ */
125
+ if (
126
+ objectHasProperty(android, 'foregroundServiceBehavior') &&
127
+ !isUndefined(android.foregroundServiceBehavior)
128
+ ) {
129
+ if (!isValidEnum(android.foregroundServiceBehavior, AndroidForegroundServiceBehavior)) {
130
+ throw new Error(
131
+ "'notification.android.foregroundServiceBehavior' expected a valid AndroidForegroundServiceBehavior.",
132
+ );
133
+ }
134
+
135
+ out.foregroundServiceBehavior = android.foregroundServiceBehavior;
136
+ }
137
+
119
138
  /**
120
139
  * lightUpScreen
121
140
  */
@@ -153,7 +172,7 @@ export default function validateAndroidNotification(
153
172
  * badgeIconType
154
173
  */
155
174
  if (objectHasProperty(android, 'badgeIconType') && !isUndefined(android.badgeIconType)) {
156
- if (!Object.values(AndroidBadgeIconType).includes(android.badgeIconType)) {
175
+ if (!isValidEnum(android.badgeIconType, AndroidBadgeIconType)) {
157
176
  throw new Error(
158
177
  "'notification.android.badgeIconType' expected a valid AndroidBadgeIconType.",
159
178
  );
@@ -239,7 +258,7 @@ export default function validateAndroidNotification(
239
258
  );
240
259
  }
241
260
 
242
- const defaults = Object.values(AndroidDefaults);
261
+ const defaults = getNumericEnumValues(AndroidDefaults);
243
262
 
244
263
  for (let i = 0; i < android.defaults.length; i++) {
245
264
  if (!defaults.includes(android.defaults[i])) {
@@ -270,7 +289,7 @@ export default function validateAndroidNotification(
270
289
  objectHasProperty(android, 'groupAlertBehavior') &&
271
290
  !isUndefined(android.groupAlertBehavior)
272
291
  ) {
273
- if (!Object.values(AndroidGroupAlertBehavior).includes(android.groupAlertBehavior)) {
292
+ if (!isValidEnum(android.groupAlertBehavior, AndroidGroupAlertBehavior)) {
274
293
  throw new Error(
275
294
  "'notification.android.groupAlertBehavior' expected a valid AndroidGroupAlertBehavior.",
276
295
  );
@@ -407,10 +426,10 @@ export default function validateAndroidNotification(
407
426
  );
408
427
  }
409
428
 
410
- const defaults = Object.values(AndroidForegroundServiceType);
429
+ const foregroundServiceTypeValues = getNumericEnumValues(AndroidForegroundServiceType);
411
430
 
412
431
  for (let i = 0; i < android.foregroundServiceTypes.length; i++) {
413
- if (!defaults.includes(android.foregroundServiceTypes[i])) {
432
+ if (!foregroundServiceTypeValues.includes(android.foregroundServiceTypes[i])) {
414
433
  throw new Error(
415
434
  "'notification.android.foregroundServiceTypes' invalid array value, expected an AndroidForegroundServiceType value.",
416
435
  );
@@ -429,13 +448,13 @@ export default function validateAndroidNotification(
429
448
  }
430
449
 
431
450
  if (android.flags.length === 0) {
432
- throw new Error("'notification.android.flags' expected an array containing AndroidDefaults.");
451
+ throw new Error("'notification.android.flags' expected an array containing AndroidFlags.");
433
452
  }
434
453
 
435
- const defaults = Object.values(AndroidFlags);
454
+ const flagValues = getNumericEnumValues(AndroidFlags);
436
455
 
437
456
  for (let i = 0; i < android.flags.length; i++) {
438
- if (!defaults.includes(android.flags[i])) {
457
+ if (!flagValues.includes(android.flags[i])) {
439
458
  throw new Error(
440
459
  "'notification.android.flags' invalid array value, expected an AndroidFlags value.",
441
460
  );
@@ -458,13 +477,33 @@ export default function validateAndroidNotification(
458
477
 
459
478
  /**
460
479
  * pressAction
480
+ *
481
+ * Defaults to { id: 'default', launchActivity: 'default' } when omitted,
482
+ * so tapping the notification opens the app. Pass pressAction: null explicitly
483
+ * to opt out (non-tappable notification).
484
+ *
485
+ * The opt-out uses a reserved sentinel id ('__NOTIFEE_OPT_OUT__') in the bundle
486
+ * sent to native, so the native layer can distinguish "absent" (synthesize default)
487
+ * from "explicitly null" (no launch intent). The same constant is defined in
488
+ * NotificationPendingIntent.java as PRESS_ACTION_OPT_OUT_ID.
461
489
  */
462
- if (objectHasProperty(android, 'pressAction') && !isUndefined(android.pressAction)) {
490
+ if (
491
+ objectHasProperty(android, 'pressAction') &&
492
+ android.pressAction !== null &&
493
+ !isUndefined(android.pressAction)
494
+ ) {
463
495
  try {
464
496
  out.pressAction = validateAndroidPressAction(android.pressAction);
465
497
  } catch (e: any) {
466
498
  throw new Error(`'notification.android.pressAction' ${e.message}`);
467
499
  }
500
+ } else if (android.pressAction === null) {
501
+ // Explicit opt-out: emit sentinel so native can distinguish from "absent".
502
+ // DO NOT use '__NOTIFEE_OPT_OUT__' as a real pressAction id — reserved sentinel.
503
+ out.pressAction = { id: '__NOTIFEE_OPT_OUT__' };
504
+ } else {
505
+ // pressAction omitted or undefined: inject default so tapping opens the app
506
+ out.pressAction = { id: 'default', launchActivity: 'default' };
468
507
  }
469
508
 
470
509
  /**
@@ -482,7 +521,7 @@ export default function validateAndroidNotification(
482
521
  * importance
483
522
  */
484
523
  if (objectHasProperty(android, 'importance') && !isUndefined(android.importance)) {
485
- if (!Object.values(AndroidImportance).includes(android.importance)) {
524
+ if (!isValidEnum(android.importance, AndroidImportance)) {
486
525
  throw new Error("'notification.android.importance' expected a valid Importance.");
487
526
  }
488
527
 
@@ -693,7 +732,7 @@ export default function validateAndroidNotification(
693
732
  * visibility
694
733
  */
695
734
  if (objectHasProperty(android, 'visibility') && android.visibility !== undefined) {
696
- if (!Object.values(AndroidVisibility).includes(android.visibility)) {
735
+ if (!isValidEnum(android.visibility, AndroidVisibility)) {
697
736
  throw new Error(
698
737
  "'notification.android.visibility' expected a valid AndroidVisibility value.",
699
738
  );
@@ -739,5 +778,19 @@ export default function validateAndroidNotification(
739
778
  out.ongoing = true;
740
779
  }
741
780
 
781
+ /**
782
+ * Auto-set foregroundServiceBehavior to IMMEDIATE for foreground service notifications.
783
+ * Only set when asForegroundService is true and the user did not provide an explicit value.
784
+ * When asForegroundService is false/undefined, strip the property entirely — it has no effect
785
+ * on non-FGS notifications and including it would bloat the Bundle unnecessarily.
786
+ */
787
+ if (out.asForegroundService) {
788
+ if (android && !objectHasProperty(android, 'foregroundServiceBehavior')) {
789
+ out.foregroundServiceBehavior = AndroidForegroundServiceBehavior.IMMEDIATE;
790
+ }
791
+ } else {
792
+ delete out.foregroundServiceBehavior;
793
+ }
794
+
742
795
  return out;
743
796
  }
@@ -28,7 +28,7 @@ export default function validateIOSPermissions(
28
28
 
29
29
  if (objectHasProperty(permissions, 'badge')) {
30
30
  if (!isBoolean(permissions.badge)) {
31
- throw new Error("'alert' badge a boolean value.");
31
+ throw new Error("'badge' expected a boolean value.");
32
32
  }
33
33
 
34
34
  out.badge = permissions.badge;
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '9.2.1';
2
+ export const version = '9.4.0';