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.
- package/README.md +9 -1
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +17 -12
- package/android/src/test/java/io/invertase/notifee/RequestPermissionThreadTest.java +102 -0
- package/cli/dist/commands/initNse.js +11 -1
- package/cli/dist/lib/notifeeExtensionFlag.js +152 -0
- package/cli/dist/lib/patchPodfile.js +3 -2
- package/cli/package.json +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +128 -72
- package/package.json +4 -3
- package/plugin/build/ios/withNotifyKitIosNseFiles.js +43 -7
- package/plugin/build/ios/withNotifyKitIosNsePodfile.js +19 -77
- package/plugin/build/shared/nse/notifeeExtensionFlag.js +155 -0
- package/plugin/build/shared/nse/patchPodfile.js +6 -64
- package/src/version.ts +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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 (
|
|
248
|
-
NotifeeCorePresentationCompletionHandler
|
|
305
|
+
} else if ([downstream respondsToSelector:_cmd]) {
|
|
306
|
+
NotifeeCorePresentationCompletionHandler done =
|
|
249
307
|
NotifeeCoreOneShotPresentationCompletionHandler(completionHandler);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
337
|
+
// External notifications prefer downstream handling in both flag states.
|
|
283
338
|
if (notifeeNotification == nil) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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.
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|