-
Notifications
You must be signed in to change notification settings - Fork 262
/
UNUserNotificationCenter+OneSignal.m
395 lines (332 loc) · 20.9 KB
/
UNUserNotificationCenter+OneSignal.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/**
* Modified MIT License
*
* Copyright 2016 OneSignal
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* 1. The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* 2. All copies of substantial portions of the Software may only be used in connection
* with services provided by OneSignal.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <UserNotifications/UserNotifications.h>
#import "UNUserNotificationCenter+OneSignal.h"
#import "OneSignal.h"
#import "OneSignalInternal.h"
#import "OneSignalHelper.h"
#import "OneSignalSelectorHelpers.h"
#import "UIApplicationDelegate+OneSignal.h"
#import "OneSignalCommonDefines.h"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
typedef void (^OSUNNotificationCenterCompletionHandler)(UNNotificationPresentationOptions options);
@interface OneSignal (UN_extra)
+ (void)notificationReceived:(NSDictionary*)messageDict wasOpened:(BOOL)opened;
+ (BOOL)shouldLogMissingPrivacyConsentErrorWithMethodName:(NSString *)methodName;
+ (void)handleWillPresentNotificationInForegroundWithPayload:(NSDictionary *)payload withCompletion:(OSNotificationDisplayResponse)completionHandler;
@end
@interface OSUNUserNotificationCenterDelegate : NSObject
+ (OSUNUserNotificationCenterDelegate*)sharedInstance;
@end
@implementation OSUNUserNotificationCenterDelegate
static OSUNUserNotificationCenterDelegate* singleInstance = nil;
+ (OSUNUserNotificationCenterDelegate*)sharedInstance {
@synchronized(singleInstance) {
if (!singleInstance)
singleInstance = [OSUNUserNotificationCenterDelegate new];
}
return singleInstance;
}
@end
// This class hooks into the following iSO 10 UNUserNotificationCenterDelegate selectors:
// - userNotificationCenter:willPresentNotification:withCompletionHandler:
// - Reads OneSignal.inFocusDisplayType to respect it's setting.
// - userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:
// - Used to process opening notifications.
//
// NOTE: On iOS 10, when a UNUserNotificationCenterDelegate is set, UIApplicationDelegate notification selectors no longer fire.
// However, this class maintains firing of UIApplicationDelegate selectors if the app did not setup it's own UNUserNotificationCenterDelegate.
// This ensures we don't produce any side effects to standard iOS API selectors.
// The `callLegacyAppDeletegateSelector` selector below takes care of this backwards compatibility handling.
@implementation OneSignalUNUserNotificationCenter
+ (void)setup {
[OneSignalUNUserNotificationCenter swizzleSelectors];
[OneSignalUNUserNotificationCenter registerDelegate];
}
static Class delegateUNClass = nil;
// Store an array of all UIAppDelegate subclasses to iterate over in cases where UIAppDelegate swizzled methods are not overriden in main AppDelegate
// But rather in one of the subclasses
static NSArray* delegateUNSubclasses = nil;
//ensures setDelegate: swizzles will never get executed twice for the same delegate object
//captures a weak reference to avoid retain cycles
__weak static id previousDelegate;
+ (void)swizzleSelectors {
injectToProperClass(@selector(setOneSignalUNDelegate:), @selector(setDelegate:), @[], [OneSignalUNUserNotificationCenter class], [UNUserNotificationCenter class]);
// Overrides to work around 10.2.1 bug where getNotificationSettingsWithCompletionHandler: reports as declined if called before
// requestAuthorizationWithOptions:'s completionHandler fires when the user accepts notifications.
injectToProperClass(@selector(onesignalRequestAuthorizationWithOptions:completionHandler:),
@selector(requestAuthorizationWithOptions:completionHandler:), @[],
[OneSignalUNUserNotificationCenter class], [UNUserNotificationCenter class]);
injectToProperClass(@selector(onesignalGetNotificationSettingsWithCompletionHandler:),
@selector(getNotificationSettingsWithCompletionHandler:), @[],
[OneSignalUNUserNotificationCenter class], [UNUserNotificationCenter class]);
}
+ (void)registerDelegate {
let curNotifCenter = [UNUserNotificationCenter currentNotificationCenter];
if (!curNotifCenter.delegate) {
/*
Set OSUNUserNotificationCenterDelegate.sharedInstance as a
UNUserNotificationCenterDelegate.
Note that OSUNUserNotificationCenterDelegate does not contain the methods such as
"willPresentNotification" as this assigment triggers setOneSignalUNDelegate which
will attach the selectors to the class at runtime.
*/
curNotifCenter.delegate = (id)OSUNUserNotificationCenterDelegate.sharedInstance;
}
else {
/*
This handles the case where a delegate may have already been assigned before
OneSignal is loaded into memory.
This re-assignment triggers setOneSignalUNDelegate providing it with the
existing delegate instance so OneSignal can swizzle in its logic.
*/
curNotifCenter.delegate = curNotifCenter.delegate;
}
}
static BOOL useiOS10_2_workaround = true;
+ (void)setUseiOS10_2_workaround:(BOOL)enable {
useiOS10_2_workaround = enable;
}
static BOOL useCachedUNNotificationSettings;
static UNNotificationSettings* cachedUNNotificationSettings;
// This is a swizzled implementation of requestAuthorizationWithOptions:
// in case developers call it directly instead of using our prompt method
- (void)onesignalRequestAuthorizationWithOptions:(UNAuthorizationOptions)options completionHandler:(void (^)(BOOL granted, NSError *__nullable error))completionHandler {
// check options for UNAuthorizationOptionProvisional membership
BOOL notProvisionalRequest = (options & PROVISIONAL_UNAUTHORIZATIONOPTION) == 0;
//we don't want to modify these settings if the authorization is provisional (iOS 12 'Direct to History')
if (notProvisionalRequest)
OneSignal.currentPermissionState.hasPrompted = true;
useCachedUNNotificationSettings = true;
id wrapperBlock = ^(BOOL granted, NSError* error) {
useCachedUNNotificationSettings = false;
if (notProvisionalRequest) {
OneSignal.currentPermissionState.accepted = granted;
OneSignal.currentPermissionState.answeredPrompt = true;
}
completionHandler(granted, error);
};
[self onesignalRequestAuthorizationWithOptions:options completionHandler:wrapperBlock];
}
- (void)onesignalGetNotificationSettingsWithCompletionHandler:(void(^)(UNNotificationSettings *settings))completionHandler {
if (useCachedUNNotificationSettings && cachedUNNotificationSettings && useiOS10_2_workaround) {
completionHandler(cachedUNNotificationSettings);
return;
}
id wrapperBlock = ^(UNNotificationSettings* settings) {
cachedUNNotificationSettings = settings;
completionHandler(settings);
};
[self onesignalGetNotificationSettingsWithCompletionHandler:wrapperBlock];
}
// Take the received delegate and swizzle in our own hooks.
// - Selector will be called once if developer does not set a UNUserNotificationCenter delegate.
// - Selector will be called a 2nd time if the developer does set one.
- (void) setOneSignalUNDelegate:(id)delegate {
if (previousDelegate == delegate) {
[self setOneSignalUNDelegate:delegate];
return;
}
previousDelegate = delegate;
[OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"OneSignalUNUserNotificationCenter setOneSignalUNDelegate Fired!"];
[OneSignalUNUserNotificationCenter swizzleSelectorsOnDelegate:delegate];
// Call orignal iOS implemenation
[self setOneSignalUNDelegate:delegate];
}
+ (void)swizzleSelectorsOnDelegate:(id)delegate {
delegateUNClass = getClassWithProtocolInHierarchy([delegate class], @protocol(UNUserNotificationCenterDelegate));
delegateUNSubclasses = ClassGetSubclasses(delegateUNClass);
injectToProperClass(@selector(onesignalUserNotificationCenter:willPresentNotification:withCompletionHandler:),
@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:), delegateUNSubclasses, [OneSignalUNUserNotificationCenter class], delegateUNClass);
injectToProperClass(@selector(onesignalUserNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:),
@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:), delegateUNSubclasses, [OneSignalUNUserNotificationCenter class], delegateUNClass);
}
+ (void)forwardNotificationWithCenter:(UNUserNotificationCenter *)center
notification:(UNNotification *)notification
OneSignalCenter:(id)instance
completionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
// Call orginal selector if one was set.
if ([instance respondsToSelector:@selector(onesignalUserNotificationCenter:willPresentNotification:withCompletionHandler:)])
[instance onesignalUserNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler];
// Or call a legacy AppDelegate selector
else {
[OneSignalUNUserNotificationCenter callLegacyAppDeletegateSelector:notification
isTextReply:false
actionIdentifier:nil
userText:nil
fromPresentNotification:true
withCompletionHandler:^() {}];
}
}
// Apple's docs - Called when a notification is delivered to a foreground app.
// NOTE: iOS behavior - Calling completionHandler with 0 means userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: does not trigger.
// - callLegacyAppDeletegateSelector is called from here due to this case.
- (void)onesignalUserNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
// return if the user has not granted privacy permissions or if not a OneSignal payload
if ([OneSignal shouldLogMissingPrivacyConsentErrorWithMethodName:nil] || ![OneSignalHelper isOneSignalPayload:notification.request.content.userInfo]) {
[OneSignalUNUserNotificationCenter forwardNotificationWithCenter:center notification:notification OneSignalCenter:self completionHandler:completionHandler];
if (![self respondsToSelector:@selector(onesignalUserNotificationCenter:willPresentNotification:withCompletionHandler:)]) {
completionHandler(7);
}
return;
}
[OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"onesignalUserNotificationCenter:willPresentNotification:withCompletionHandler: Fired! %@", notification.request.content.body]];
[OneSignal handleWillPresentNotificationInForegroundWithPayload:notification.request.content.userInfo withCompletion:^(OSNotification *responseNotif) {
UNNotificationPresentationOptions displayType = responseNotif != nil ? (UNNotificationPresentationOptions)7 : (UNNotificationPresentationOptions)0;
finishProcessingNotification(notification, center, displayType, completionHandler, self);
}];
}
// To avoid a crash caused by using the swizzled OneSignalUNUserNotificationCenter type this is implemented as a C function
void finishProcessingNotification(UNNotification *notification,
UNUserNotificationCenter *center,
UNNotificationPresentationOptions displayType,
OSUNNotificationCenterCompletionHandler completionHandler,
OneSignalUNUserNotificationCenter *instance) {
[OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"finishProcessingNotification: Fired!"];
NSUInteger completionHandlerOptions = displayType;
[OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"Notification display type: %lu", (unsigned long)displayType]];
if ([OneSignal appId])
[OneSignal notificationReceived:notification.request.content.userInfo wasOpened:NO];
[OneSignalUNUserNotificationCenter forwardNotificationWithCenter:center notification:notification OneSignalCenter:instance completionHandler:completionHandler];
// Calling completionHandler for the following reasons:
// App dev may have not implented userNotificationCenter:willPresentNotification.
// App dev may have implemented this selector but forgot to call completionHandler().
// Note - iOS only uses the first call to completionHandler().
[OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"finishProcessingNotification: call completionHandler with options: %lu",(unsigned long)completionHandlerOptions]];
completionHandler(completionHandlerOptions);
}
// Apple's docs - Called to let your app know which action was selected by the user for a given notification.
- (void)onesignalUserNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void(^)())completionHandler {
// return if the user has not granted privacy permissions or if not a OneSignal payload
if ([OneSignal shouldLogMissingPrivacyConsentErrorWithMethodName:nil] || ![OneSignalHelper isOneSignalPayload:response.notification.request.content.userInfo]) {
if ([self respondsToSelector:@selector(onesignalUserNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)])
[self onesignalUserNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
else
completionHandler();
return;
}
[OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"onesignalUserNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: Fired!"];
[OneSignalUNUserNotificationCenter processiOS10Open:response];
// Call orginal selector if one was set.
if ([self respondsToSelector:@selector(onesignalUserNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)])
[self onesignalUserNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
// Or call a legacy AppDelegate selector
// - If not a dismiss event as their isn't a iOS 9 selector for it.
else if (![OneSignalUNUserNotificationCenter isDismissEvent:response]) {
BOOL isTextReply = [response isKindOfClass:NSClassFromString(@"UNTextInputNotificationResponse")];
NSString* userText = isTextReply ? [response valueForKey:@"userText"] : nil;
[OneSignalUNUserNotificationCenter callLegacyAppDeletegateSelector:response.notification
isTextReply:isTextReply
actionIdentifier:response.actionIdentifier
userText:userText
fromPresentNotification:false
withCompletionHandler:completionHandler];
}
else
completionHandler();
}
+ (BOOL)isDismissEvent:(UNNotificationResponse *)response {
return [@"com.apple.UNNotificationDismissActionIdentifier" isEqual:response.actionIdentifier];
}
+ (void)processiOS10Open:(UNNotificationResponse*)response {
if (![OneSignal appId])
return;
if ([OneSignalUNUserNotificationCenter isDismissEvent:response])
return;
if (![OneSignalHelper isOneSignalPayload:response.notification.request.content.userInfo])
return;
let userInfo = [OneSignalHelper formatApsPayloadIntoStandard:response.notification.request.content.userInfo
identifier:response.actionIdentifier];
[OneSignal notificationReceived:userInfo wasOpened:YES];
}
// Calls depercated pre-iOS 10 selector if one is set on the AppDelegate.
// Even though they are deperated in iOS 10 they should still be called in iOS 10
// As long as they didn't setup their own UNUserNotificationCenterDelegate
// - application:didReceiveLocalNotification:
// - application:didReceiveRemoteNotification:fetchCompletionHandler:
// - application:handleActionWithIdentifier:forLocalNotification:withResponseInfo:completionHandler:
// - application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:
// - application:handleActionWithIdentifier:forLocalNotification:completionHandler:
// - application:handleActionWithIdentifier:forRemoteNotification:completionHandler:
+ (void)callLegacyAppDeletegateSelector:(UNNotification *)notification
isTextReply:(BOOL)isTextReply
actionIdentifier:(NSString*)actionIdentifier
userText:(NSString*)userText
fromPresentNotification:(BOOL)fromPresentNotification
withCompletionHandler:(void(^)())completionHandler {
[OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"callLegacyAppDeletegateSelector:withCompletionHandler: Fired!"];
UIApplication *sharedApp = [UIApplication sharedApplication];
/*
The iOS SDK used to call some local notification selectors (such as didReceiveLocalNotification)
as a convenience but has stopped due to concerns about private API usage
the SDK will now print warnings when a developer's app implements these selectors
*/
BOOL isCustomAction = actionIdentifier && ![@"com.apple.UNNotificationDefaultActionIdentifier" isEqualToString:actionIdentifier];
BOOL isRemote = [notification.request.trigger isKindOfClass:NSClassFromString(@"UNPushNotificationTrigger")];
if (isRemote) {
NSDictionary* remoteUserInfo = notification.request.content.userInfo;
if (isTextReply &&
[sharedApp.delegate respondsToSelector:@selector(application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:)]) {
NSDictionary* responseInfo = @{UIUserNotificationActionResponseTypedTextKey: userText};
[sharedApp.delegate application:sharedApp handleActionWithIdentifier:actionIdentifier forRemoteNotification:remoteUserInfo withResponseInfo:responseInfo completionHandler:^() {
completionHandler();
}];
}
else if (isCustomAction &&
[sharedApp.delegate respondsToSelector:@selector(application:handleActionWithIdentifier:forRemoteNotification:completionHandler:)])
[sharedApp.delegate application:sharedApp handleActionWithIdentifier:actionIdentifier forRemoteNotification:remoteUserInfo completionHandler:^() {
completionHandler();
}];
// Always trigger selector for open events and for non-content-available receive events.
// content-available seems to be an odd expection to iOS 10's fallback rules for legacy selectors.
else if ([sharedApp.delegate respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)] &&
(!fromPresentNotification ||
![[notification.request.trigger valueForKey:@"_isContentAvailable"] boolValue])) {
// NOTE: Should always be true as our AppDelegate swizzling should be there unless something else unswizzled it.
[sharedApp.delegate application:sharedApp didReceiveRemoteNotification:remoteUserInfo fetchCompletionHandler:^(UIBackgroundFetchResult result) {
// Call iOS 10's compleationHandler from iOS 9's completion handler.
completionHandler();
}];
}
else
completionHandler();
}
else
completionHandler();
}
@end
#pragma clang diagnostic pop
#pragma clang diagnostic pop