react-native-notify-kit 10.10.0 → 10.10.1

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.
@@ -17,6 +17,8 @@
17
17
 
18
18
  #import "NotifeeCore+UNUserNotificationCenter.h"
19
19
 
20
+ #import <objc/runtime.h>
21
+
20
22
  #import "NotifeeCore.h"
21
23
  #import "NotifeeCoreDelegateHolder.h"
22
24
  #import "NotifeeCoreUtil.h"
@@ -29,7 +31,6 @@ typedef void (^NotifeeCoreVoidCompletionHandler)(void);
29
31
  @end
30
32
 
31
33
  @interface NotifeeCoreUNUserNotificationCenter ()
32
- - (void)refreshOriginalDelegateSelectorFlags;
33
34
  - (void)rechainUserNotificationCenterDelegate;
34
35
  @end
35
36
 
@@ -78,13 +79,85 @@ static NotifeeCoreVoidCompletionHandler NotifeeCoreOneShotVoidCompletionHandler(
78
79
  return [oneShotCompletionHandler copy];
79
80
  }
80
81
 
81
- @implementation NotifeeCoreUNUserNotificationCenter
82
+ // Class/ISA pointers differ when two images contain this implementation. Use
83
+ // runtime names, then message the trusted peer's accessor; never read its ivars.
84
+ static BOOL NotifeeCoreIsDelegatePeer(id delegate) {
85
+ for (Class cls = object_getClass(delegate); cls != Nil; cls = class_getSuperclass(cls)) {
86
+ if (strcmp(class_getName(cls), "NotifeeCoreUNUserNotificationCenter") == 0) return YES;
87
+ }
88
+ return NO;
89
+ }
90
+
91
+ @protocol NotifeeCoreDelegateChain <NSObject>
92
+ - (id<UNUserNotificationCenterDelegate>)originalDelegate;
93
+ @end
94
+
95
+ static BOOL NotifeeCoreDelegateChainIsUnsafe(id owner, id candidate) {
96
+ NSMutableArray *visited = [NSMutableArray new];
97
+ id cursor = candidate;
98
+ for (NSUInteger hops = 0; cursor != nil; hops++) {
99
+ if (cursor == owner || [visited indexOfObjectIdenticalTo:cursor] != NSNotFound) return YES;
100
+ if (!NotifeeCoreIsDelegatePeer(cursor)) return NO; // Opaque SDK: contain only on reentry.
101
+ if (hops >= 32) return YES; // Bounded, fail closed.
102
+ [visited addObject:cursor];
103
+ cursor = [(id<NotifeeCoreDelegateChain>)cursor originalDelegate];
104
+ }
105
+ return NO;
106
+ }
82
107
 
83
- struct {
84
- unsigned int willPresentNotification : 1;
85
- unsigned int didReceiveNotificationResponse : 1;
86
- unsigned int openSettingsForNotification : 1;
87
- } originalUNCDelegateRespondsTo;
108
+ // Foundation containers and a versioned string key share the synchronous call
109
+ // context across binary images, without relying on a per-image registry/Class.
110
+ // Identity includes selector, center and event, so nested distinct notifications
111
+ // and later deliveries of the same object are independent. No state survives an
112
+ // unwind; ordinary asynchronous downstream completions remain unrestricted.
113
+ static void NotifeeCoreForward(id owner, id downstream, id center, id event, SEL selector,
114
+ void (^invoke)(void), void (^fallback)(void)) {
115
+ NSString *key = @"app.notifee.UNUserNotificationCenter.forwarding.v1";
116
+ NSMutableDictionary *threadState = [NSThread currentThread].threadDictionary;
117
+ NSMutableArray *frames = threadState[key];
118
+ for (NSMutableDictionary *active in frames) {
119
+ if (active[@"owner"] == owner && active[@"center"] == (center ?: [NSNull null]) &&
120
+ active[@"event"] == (event ?: [NSNull null]) &&
121
+ [active[@"selector"] isEqualToString:NSStringFromSelector(selector)]) {
122
+ active[@"cycle"] = @YES;
123
+ // The outer invocation completes after the SDK returns, allowing an SDK
124
+ // to supply its own options before or after the recursive call unwinds.
125
+ return;
126
+ }
127
+ }
128
+ if (NotifeeCoreDelegateChainIsUnsafe(owner, downstream)) {
129
+ fallback();
130
+ return;
131
+ }
132
+ if (frames == nil) {
133
+ frames = [NSMutableArray new];
134
+ threadState[key] = frames;
135
+ }
136
+ NSMutableDictionary *frame = [@{
137
+ @"owner" : owner,
138
+ @"center" : center ?: [NSNull null],
139
+ @"event" : event ?: [NSNull null],
140
+ @"selector" : NSStringFromSelector(selector),
141
+ @"cycle" : @NO
142
+ } mutableCopy];
143
+ [frames addObject:frame];
144
+ @try {
145
+ invoke();
146
+ // Only a proven synchronous cycle triggers fallback. The caller passes a
147
+ // one-shot block, preserving any options already supplied by the downstream.
148
+ if ([frame[@"cycle"] boolValue]) fallback();
149
+ } @finally {
150
+ [frames removeObjectIdenticalTo:frame];
151
+ if (frames.count == 0) [threadState removeObjectForKey:key];
152
+ }
153
+ }
154
+
155
+ static UNNotificationPresentationOptions NotifeeCoreDefaultPresentationOptions(void) {
156
+ return UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound |
157
+ UNNotificationPresentationOptionList | UNNotificationPresentationOptionBadge;
158
+ }
159
+
160
+ @implementation NotifeeCoreUNUserNotificationCenter
88
161
 
89
162
  + (instancetype)instance {
90
163
  static dispatch_once_t once;
@@ -103,36 +176,20 @@ struct {
103
176
  [self rechainUserNotificationCenterDelegate];
104
177
  }
105
178
 
106
- - (void)refreshOriginalDelegateSelectorFlags {
107
- id<UNUserNotificationCenterDelegate> originalDelegate = self.originalDelegate;
108
-
109
- originalUNCDelegateRespondsTo.openSettingsForNotification =
110
- originalDelegate != nil &&
111
- [originalDelegate respondsToSelector:@selector(userNotificationCenter:
112
- openSettingsForNotification:)];
113
- originalUNCDelegateRespondsTo.willPresentNotification =
114
- originalDelegate != nil &&
115
- [originalDelegate respondsToSelector:@selector
116
- (userNotificationCenter:willPresentNotification:withCompletionHandler:)];
117
- originalUNCDelegateRespondsTo.didReceiveNotificationResponse =
118
- originalDelegate != nil &&
119
- [originalDelegate
120
- respondsToSelector:@selector(userNotificationCenter:
121
- didReceiveNotificationResponse:withCompletionHandler:)];
122
- }
123
-
124
179
  - (void)rechainUserNotificationCenterDelegate {
125
180
  @synchronized(self) {
126
181
  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
127
182
  id<UNUserNotificationCenterDelegate> currentDelegate = center.delegate;
183
+ if (currentDelegate == self) return;
128
184
 
129
- if (currentDelegate == self) {
130
- [self refreshOriginalDelegateSelectorFlags];
185
+ // Leave an installed peer in charge. Replacing it loses its healthy leaf
186
+ // and permits independent singletons to adopt each other on activation.
187
+ // Preserve our existing downstream when a candidate is demonstrably unsafe.
188
+ if (NotifeeCoreIsDelegatePeer(currentDelegate) ||
189
+ NotifeeCoreDelegateChainIsUnsafe(self, currentDelegate))
131
190
  return;
132
- }
133
191
 
134
192
  self.originalDelegate = currentDelegate;
135
- [self refreshOriginalDelegateSelectorFlags];
136
193
  center.delegate = self;
137
194
  }
138
195
  }
@@ -188,6 +245,7 @@ struct {
188
245
  willPresentNotification:(UNNotification *)notification
189
246
  withCompletionHandler:
190
247
  (void (^)(UNNotificationPresentationOptions options))completionHandler {
248
+ id<UNUserNotificationCenterDelegate> downstream = self.originalDelegate;
191
249
  NSDictionary *notifeeNotification =
192
250
  notification.request.content.userInfo[kNotifeeUserInfoNotification];
193
251
  BOOL isRollingTimestampNotification =
@@ -244,24 +302,21 @@ struct {
244
302
  [self topUpRollingTimestampTriggersForLifecycleEvent:@"foreground delivery"];
245
303
  }
246
304
 
247
- } else if (_originalDelegate != nil && originalUNCDelegateRespondsTo.willPresentNotification) {
248
- NotifeeCorePresentationCompletionHandler oneShotCompletionHandler =
305
+ } else if ([downstream respondsToSelector:_cmd]) {
306
+ NotifeeCorePresentationCompletionHandler done =
249
307
  NotifeeCoreOneShotPresentationCompletionHandler(completionHandler);
250
- [_originalDelegate userNotificationCenter:center
251
- willPresentNotification:notification
252
- withCompletionHandler:oneShotCompletionHandler];
308
+ NotifeeCoreForward(
309
+ self, downstream, center, notification, _cmd,
310
+ ^{
311
+ [downstream userNotificationCenter:center
312
+ willPresentNotification:notification
313
+ withCompletionHandler:done];
314
+ },
315
+ ^{
316
+ done(NotifeeCoreDefaultPresentationOptions());
317
+ });
253
318
  } else {
254
- // No original delegate captured and the notification is not Notifee-owned.
255
- // Returning UNNotificationPresentationOptionNone would silently drop the
256
- // notification (no banner, no sound, no badge, no Notification Center entry),
257
- // making remote pushes invisible to users in apps that don't install a
258
- // competing UNUserNotificationCenterDelegate (e.g. apps without
259
- // @react-native-firebase/messaging). Fall back to the platform default
260
- // presentation options instead so the system shows the notification as it
261
- // would if Notifee had not installed a delegate at all.
262
- completionHandler(UNNotificationPresentationOptionBanner |
263
- UNNotificationPresentationOptionSound | UNNotificationPresentationOptionList |
264
- UNNotificationPresentationOptionBadge);
319
+ completionHandler(NotifeeCoreDefaultPresentationOptions());
265
320
  }
266
321
  }
267
322
 
@@ -279,34 +334,30 @@ struct {
279
334
 
280
335
  _notificationOpenedAppID = notifeeNotification[@"id"];
281
336
 
282
- // handle notification outside of notifee
337
+ // External notifications prefer downstream handling in both flag states.
283
338
  if (notifeeNotification == nil) {
284
- if (!self.shouldHandleRemoteNotifications) {
285
- // Flag OFF: always forward to original delegate, never parse as Notifee
286
- if (_originalDelegate != nil &&
287
- originalUNCDelegateRespondsTo.didReceiveNotificationResponse) {
288
- NotifeeCoreVoidCompletionHandler oneShotCompletionHandler =
289
- NotifeeCoreOneShotVoidCompletionHandler(completionHandler);
290
- [_originalDelegate userNotificationCenter:center
291
- didReceiveNotificationResponse:response
292
- withCompletionHandler:oneShotCompletionHandler];
293
- } else {
294
- completionHandler();
295
- }
339
+ id<UNUserNotificationCenterDelegate> downstream = self.originalDelegate;
340
+ if ([downstream respondsToSelector:_cmd]) {
341
+ NotifeeCoreVoidCompletionHandler done =
342
+ NotifeeCoreOneShotVoidCompletionHandler(completionHandler);
343
+ NotifeeCoreForward(
344
+ self, downstream, center, response, _cmd,
345
+ ^{
346
+ [downstream userNotificationCenter:center
347
+ didReceiveNotificationResponse:response
348
+ withCompletionHandler:done];
349
+ },
350
+ ^{
351
+ done();
352
+ });
296
353
  return;
297
354
  }
298
- // Flag ON (default): existing behavior
299
- if (_originalDelegate != nil && originalUNCDelegateRespondsTo.didReceiveNotificationResponse) {
300
- NotifeeCoreVoidCompletionHandler oneShotCompletionHandler =
301
- NotifeeCoreOneShotVoidCompletionHandler(completionHandler);
302
- [_originalDelegate userNotificationCenter:center
303
- didReceiveNotificationResponse:response
304
- withCompletionHandler:oneShotCompletionHandler];
355
+ if (!self.shouldHandleRemoteNotifications) {
356
+ completionHandler();
305
357
  return;
306
- } else {
307
- notifeeNotification =
308
- [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
309
358
  }
359
+ notifeeNotification =
360
+ [NotifeeCoreUtil parseUNNotificationRequest:response.notification.request];
310
361
  }
311
362
 
312
363
  if (notifeeNotification != nil) {
@@ -375,11 +426,16 @@ struct {
375
426
 
376
427
  - (void)userNotificationCenter:(UNUserNotificationCenter *)center
377
428
  openSettingsForNotification:(nullable UNNotification *)notification {
378
- if (_originalDelegate != nil && originalUNCDelegateRespondsTo.openSettingsForNotification) {
429
+ id<UNUserNotificationCenterDelegate> downstream = self.originalDelegate;
430
+ if ([downstream respondsToSelector:_cmd]) {
379
431
  if (@available(iOS 12.0, macOS 10.14, macCatalyst 13.0, *)) {
380
- [_originalDelegate userNotificationCenter:center openSettingsForNotification:notification];
381
- } else {
382
- // Fallback on earlier versions
432
+ NotifeeCoreForward(self, downstream, center, notification, _cmd,
433
+ ^{
434
+ [downstream userNotificationCenter:center
435
+ openSettingsForNotification:notification];
436
+ },
437
+ ^{
438
+ });
383
439
  }
384
440
  }
385
441
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-notify-kit",
3
- "version": "10.10.0",
3
+ "version": "10.10.1",
4
4
  "author": "Marco Crupi",
5
5
  "description": "Maintained Notifee-compatible React Native notifications library for Android, iOS, FCM Mode, rich notifications, foreground services, and Expo CNG development builds.",
6
6
  "main": "dist/index.js",
@@ -26,13 +26,14 @@
26
26
  },
27
27
  "scripts": {
28
28
  "validate:all:ts": "tsc --project ./",
29
- "build": "genversion --es6 --semi src/version.ts && tsc",
29
+ "build": "genversion --es6 --semi src/version.ts && tsc && yarn run build:nse-podfile",
30
+ "build:nse-podfile": "node ../../scripts/build-nse-podfile.cjs",
30
31
  "build:server": "tsc -p server/tsconfig.json",
31
32
  "build:clean": "rimraf android/build && rimraf ios/build && rimraf dist && rimraf server/dist",
32
33
  "build:watch": "tsc --watch",
33
34
  "test:server": "jest --config jest.config.server.js",
34
35
  "prepare": "yarn run build",
35
- "prepack": "yarn run build:server && cd ../.. && bash build_ios_core.sh && bash scripts/verify-ios-core-generation.sh && bash scripts/prepack-cli.sh",
36
+ "prepack": "yarn run build:nse-podfile && yarn run build:server && cd ../.. && bash build_ios_core.sh && bash scripts/verify-ios-core-generation.sh && bash scripts/prepack-cli.sh",
36
37
  "postpack": "node ../../scripts/cli-artifacts.cjs postpack",
37
38
  "prepublishOnly": "cd ../.. && yarn run build:core",
38
39
  "format:android": "google-java-format --replace -i $(find . -type f -name \"*.java\" ! -path \"*/node_modules/*\" ! -path \"*/generated/*\")",
@@ -52,7 +52,7 @@ function writeNotifyKitIosNseFiles(
52
52
  writeFileIfMissingOrIdentical(
53
53
  path.join(targetDir, 'Info.plist'),
54
54
  renderNseInfoPlist({ targetName, ...versionOptions }),
55
- contents => isGeneratedNseInfoPlist(contents, targetName),
55
+ contents => reconcileGeneratedNseInfoPlist(contents, targetName, versionOptions),
56
56
  );
57
57
  writeFileIfMissingOrIdentical(
58
58
  path.join(targetDir, `${targetName}.entitlements`),
@@ -72,29 +72,65 @@ function optionalString(value) {
72
72
  return typeof value === 'string' ? value : undefined;
73
73
  }
74
74
 
75
- function isGeneratedNseInfoPlist(contents, targetName) {
75
+ // React Native's post_install (NewArchitectureHelper.set_RCTNewArchEnabled_in_info_plist)
76
+ // appends this exact top-level entry to every target Info.plist, including the NSE.
77
+ // Recognize only its canonical serialization immediately before the root </dict>, and
78
+ // carry it over unchanged; NotifyKit never creates, removes or edits it.
79
+ const RN_NEW_ARCH_ENABLED_ENTRY_PATTERN =
80
+ '(\\t<key>RCTNewArchEnabled</key>\\n\\t<(?:true|false)/>\\n)?';
81
+ const ROOT_DICT_END = '</dict>\n</plist>\n';
82
+
83
+ // Returns the contents to keep for a NotifyKit-generated Info.plist (refreshed version
84
+ // fields plus any preserved tooling entry), or null when the file has consumer changes.
85
+ function reconcileGeneratedNseInfoPlist(contents, targetName, versionOptions) {
86
+ const toolingEntry = matchGeneratedNseInfoPlist(contents, targetName);
87
+ if (toolingEntry === null) {
88
+ return null;
89
+ }
90
+
91
+ const generated = renderNseInfoPlist({ targetName, ...versionOptions });
92
+ if (!generated.endsWith(ROOT_DICT_END)) {
93
+ return null;
94
+ }
95
+
96
+ return generated.slice(0, -ROOT_DICT_END.length) + toolingEntry + ROOT_DICT_END;
97
+ }
98
+
99
+ // Returns the exact optional tooling entry ('' when absent), or null when the contents
100
+ // are not the generated template.
101
+ function matchGeneratedNseInfoPlist(contents, targetName) {
76
102
  const templateWithMarkers = renderNseInfoPlist({
77
103
  targetName,
78
104
  marketingVersion: MARKETING_VERSION_MARKER,
79
105
  currentProjectVersion: CURRENT_PROJECT_VERSION_MARKER,
80
106
  });
81
- const escapedTemplate = escapeRegExp(templateWithMarkers)
107
+ if (!templateWithMarkers.endsWith(ROOT_DICT_END)) {
108
+ return null;
109
+ }
110
+
111
+ const escapedTemplateBody = escapeRegExp(templateWithMarkers.slice(0, -ROOT_DICT_END.length))
82
112
  .replace(MARKETING_VERSION_MARKER, '[^<]*')
83
113
  .replace(CURRENT_PROJECT_VERSION_MARKER, '[^<]*');
114
+ const match = new RegExp(
115
+ `^${escapedTemplateBody}${RN_NEW_ARCH_ENABLED_ENTRY_PATTERN}${escapeRegExp(ROOT_DICT_END)}$`,
116
+ ).exec(contents);
84
117
 
85
- return new RegExp(`^${escapedTemplate}$`).test(contents);
118
+ return match ? (match[1] ?? '') : null;
86
119
  }
87
120
 
88
121
  function escapeRegExp(value) {
89
122
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
90
123
  }
91
124
 
92
- function writeFileIfMissingOrIdentical(filePath, contents, canOverwrite) {
125
+ function writeFileIfMissingOrIdentical(filePath, contents, reconcile) {
93
126
  if (fs.existsSync(filePath)) {
94
127
  const currentContents = fs.readFileSync(filePath, 'utf8');
95
128
  if (currentContents !== contents) {
96
- if (canOverwrite?.(currentContents)) {
97
- fs.writeFileSync(filePath, contents, 'utf8');
129
+ const reconciledContents = reconcile?.(currentContents) ?? null;
130
+ if (reconciledContents !== null) {
131
+ if (reconciledContents !== currentContents) {
132
+ fs.writeFileSync(filePath, reconciledContents, 'utf8');
133
+ }
98
134
  return;
99
135
  }
100
136