Skip to content

Commit 7142c7f

Browse files
authored
Save categories (#481)
* Persist Category ID's • We recently discovered an issue where if an app sends two notifications that have their own unique buttons, both notifications will display whatever the more recent notifications buttons were supposed to be. • For example, if an app sends a notification with "Like" as the button title, and then later on it sends another notification with "Dislike" as the title, BOTH notifications will now show "Dislike" as the button title. • This was caused by the fact that the SDK uses a single UNNotificationCategory ID called "__dynamic__" to register notification actions, meaning the most recently received notification defines the buttons. • Fixes the issue by now registering unique UNNotificationCategory's to each specific notification. • To prevent the steady buildup of registered categories over time, we define a limit (MAX_CATEGORIES_SIZE, which is currently 128) and delete/de-register any previous UNNotificationCategory's than the 128 most recent. • Adds a test to make sure that the SDK is correctly registering, deleting, and associating categories with notifications * Fix Old Test • Fixes an old test that depended on the old statically defined UNNotificationCategory identifier * Move Method • Moves the existingCategories() method from OneSignalHelper to OneSignalNotificationCategoryController
1 parent a807ba8 commit 7142c7f

File tree

7 files changed

+262
-32
lines changed

7 files changed

+262
-32
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@
188188
CAABF34B205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */; };
189189
CAABF34C205B157B0042F8E5 /* OneSignalExtensionBadgeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */; };
190190
CAABF34D205B157B0042F8E5 /* OneSignalExtensionBadgeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */; };
191+
CAAEA68721ED68A40049CF15 /* OneSignalNotificationCategoryController.m in Sources */ = {isa = PBXBuildFile; fileRef = CAAEA68521ED68A30049CF15 /* OneSignalNotificationCategoryController.m */; };
192+
CAAEA68821ED68A40049CF15 /* OneSignalNotificationCategoryController.m in Sources */ = {isa = PBXBuildFile; fileRef = CAAEA68521ED68A30049CF15 /* OneSignalNotificationCategoryController.m */; };
193+
CAAEA68921ED68A40049CF15 /* OneSignalNotificationCategoryController.m in Sources */ = {isa = PBXBuildFile; fileRef = CAAEA68521ED68A30049CF15 /* OneSignalNotificationCategoryController.m */; };
194+
CAAEA68A21ED68A40049CF15 /* OneSignalNotificationCategoryController.h in Headers */ = {isa = PBXBuildFile; fileRef = CAAEA68621ED68A40049CF15 /* OneSignalNotificationCategoryController.h */; };
191195
CAB4112920852E48005A70D1 /* DelayedInitializationParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */; };
192196
CAB4112A20852E4C005A70D1 /* DelayedInitializationParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */; };
193197
CAB4112B20852E4C005A70D1 /* DelayedInitializationParameters.m in Sources */ = {isa = PBXBuildFile; fileRef = CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */; };
@@ -331,6 +335,8 @@
331335
CAA4ED0020646762005BD59B /* BadgeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BadgeTests.m; sourceTree = "<group>"; };
332336
CAABF349205B15780042F8E5 /* OneSignalExtensionBadgeHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalExtensionBadgeHandler.h; sourceTree = "<group>"; };
333337
CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalExtensionBadgeHandler.m; sourceTree = "<group>"; };
338+
CAAEA68521ED68A30049CF15 /* OneSignalNotificationCategoryController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OneSignalNotificationCategoryController.m; sourceTree = "<group>"; };
339+
CAAEA68621ED68A40049CF15 /* OneSignalNotificationCategoryController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OneSignalNotificationCategoryController.h; sourceTree = "<group>"; };
334340
CAB4112720852E48005A70D1 /* DelayedInitializationParameters.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DelayedInitializationParameters.h; sourceTree = "<group>"; };
335341
CAB4112820852E48005A70D1 /* DelayedInitializationParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DelayedInitializationParameters.m; sourceTree = "<group>"; };
336342
CAB411AC208931EE005A70D1 /* DummyNotificationCenterDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DummyNotificationCenterDelegate.h; sourceTree = "<group>"; };
@@ -514,6 +520,8 @@
514520
454F94F11FAD218000D74CCF /* OneSignalNotificationServiceExtensionHandler.m */,
515521
CAABF349205B15780042F8E5 /* OneSignalExtensionBadgeHandler.h */,
516522
CAABF34A205B15780042F8E5 /* OneSignalExtensionBadgeHandler.m */,
523+
CAAEA68621ED68A40049CF15 /* OneSignalNotificationCategoryController.h */,
524+
CAAEA68521ED68A30049CF15 /* OneSignalNotificationCategoryController.m */,
517525
);
518526
path = Source;
519527
sourceTree = "<group>";
@@ -627,6 +635,7 @@
627635
9124121D1E73342200E41FD7 /* OneSignalJailbreakDetection.h in Headers */,
628636
9129C6B71E89E59B009CB6A0 /* OSPermission.h in Headers */,
629637
912412151E73342200E41FD7 /* OneSignalHelper.h in Headers */,
638+
CAAEA68A21ED68A40049CF15 /* OneSignalNotificationCategoryController.h in Headers */,
630639
91F58D7F1E7C7F5F0017D24D /* OneSignalNotificationSettingsIOS10.h in Headers */,
631640
912412391E73342200E41FD7 /* OneSignalWebView.h in Headers */,
632641
91C7725E1E7CCE1000D612D0 /* OneSignalInternal.h in Headers */,
@@ -796,6 +805,7 @@
796805
9124120E1E73342200E41FD7 /* OneSignal.m in Sources */,
797806
CA08FC731FE99AFD004C445F /* OneSignalClient.m in Sources */,
798807
91F58D831E7C80DA0017D24D /* OneSignalNotificationSettingsIOS8.m in Sources */,
808+
CAAEA68721ED68A40049CF15 /* OneSignalNotificationCategoryController.m in Sources */,
799809
9124121E1E73342200E41FD7 /* OneSignalJailbreakDetection.m in Sources */,
800810
CA08FC791FE99B13004C445F /* OneSignalRequest.m in Sources */,
801811
912412471E73369600E41FD7 /* OneSignalHelper.m in Sources */,
@@ -837,6 +847,7 @@
837847
9124120F1E73342200E41FD7 /* OneSignal.m in Sources */,
838848
CA08FC741FE99AFF004C445F /* OneSignalClient.m in Sources */,
839849
91F58D861E7C88250017D24D /* OneSignalNotificationSettingsIOS8.m in Sources */,
850+
CAAEA68821ED68A40049CF15 /* OneSignalNotificationCategoryController.m in Sources */,
840851
9124121F1E73342200E41FD7 /* OneSignalJailbreakDetection.m in Sources */,
841852
CA08FC7A1FE99B13004C445F /* OneSignalRequest.m in Sources */,
842853
912412481E73369700E41FD7 /* OneSignalHelper.m in Sources */,
@@ -879,6 +890,7 @@
879890
91F58D8B1E7C9A240017D24D /* OneSignalNotificationSettingsIOS7.m in Sources */,
880891
91F60F7D1E80E4E400706E60 /* UncaughtExceptionHandler.m in Sources */,
881892
912412201E73342200E41FD7 /* OneSignalJailbreakDetection.m in Sources */,
893+
CAAEA68921ED68A40049CF15 /* OneSignalNotificationCategoryController.m in Sources */,
882894
CA85C15320604AEA003AB529 /* RequestTests.m in Sources */,
883895
CAE2E5A8215D80010036FD32 /* OneSignalTrackFirebaseAnalytics.m in Sources */,
884896
912412381E73342200E41FD7 /* OneSignalTrackIAP.m in Sources */,

iOS_SDK/OneSignalSDK/Source/OneSignalCommonDefines.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE} HTTPMethod;
114114
// before registering the user anyways
115115
#define APNS_TIMEOUT 25.0
116116

117+
// The SDK saves a list of category ID's allowing multiple notifications
118+
// to have their own unique buttons/etc.
119+
#define SHARED_CATEGORY_LIST @"com.onesignal.shared_registered_categories"
120+
117121
#ifndef OS_TEST
118122
// OneSignal API Client Defines
119123
#define REATTEMPT_DELAY 30.0
@@ -123,6 +127,9 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE} HTTPMethod;
123127

124128
// Send tags batch delay
125129
#define SEND_TAGS_DELAY 5.0
130+
131+
// the max number of UNNotificationCategory ID's the SDK will register
132+
#define MAX_CATEGORIES_SIZE 128
126133
#else
127134
// Test defines for API Client
128135
#define REATTEMPT_DELAY 0.004
@@ -132,6 +139,9 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE} HTTPMethod;
132139

133140
// Send tags batch delay
134141
#define SEND_TAGS_DELAY 0.005
142+
143+
// the max number of UNNotificationCategory ID's the SDK will register
144+
#define MAX_CATEGORIES_SIZE 5
135145
#endif
136146

137147
// A max timeout for a request, which might include multiple reattempts

iOS_SDK/OneSignalSDK/Source/OneSignalHelper.m

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#import "NSURL+OneSignal.h"
3939
#import "OneSignalCommonDefines.h"
4040
#import "OneSignalDialogController.h"
41+
#import "OneSignalNotificationCategoryController.h"
4142

4243
#define NOTIFICATION_TYPE_ALL 7
4344
#pragma clang diagnostic push
@@ -596,17 +597,19 @@ + (void)addActionButtons:(OSNotificationPayload*)payload
596597
finalActionArray = actionArray;
597598

598599
// Get a full list of categories so we don't replace any exisiting ones.
599-
var allCategories = [self existingCategories];
600+
var allCategories = OneSignalNotificationCategoryController.sharedInstance.existingCategories;
600601

601-
let category = [UNNotificationCategory categoryWithIdentifier:@"__dynamic__"
602+
let newCategoryIdentifier = [OneSignalNotificationCategoryController.sharedInstance registerNotificationCategoryForNotificationId:payload.notificationID];
603+
604+
let category = [UNNotificationCategory categoryWithIdentifier:newCategoryIdentifier
602605
actions:finalActionArray
603606
intentIdentifiers:@[]
604607
options:UNNotificationCategoryOptionCustomDismissAction];
605608

606609
if (allCategories) {
607610
let newCategorySet = [NSMutableSet new];
608611
for(UNNotificationCategory *existingCategory in allCategories) {
609-
if (![existingCategory.identifier isEqualToString:@"__dynamic__"])
612+
if (![existingCategory.identifier isEqualToString:newCategoryIdentifier])
610613
[newCategorySet addObject:existingCategory];
611614
}
612615

@@ -618,20 +621,7 @@ + (void)addActionButtons:(OSNotificationPayload*)payload
618621

619622
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:allCategories];
620623

621-
content.categoryIdentifier = @"__dynamic__";
622-
}
623-
624-
+ (NSMutableSet<UNNotificationCategory*>*)existingCategories {
625-
__block NSMutableSet* allCategories;
626-
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
627-
let notificationCenter = [UNUserNotificationCenter currentNotificationCenter];
628-
[notificationCenter getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
629-
allCategories = [categories mutableCopy];
630-
dispatch_semaphore_signal(semaphore);
631-
}];
632-
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
633-
634-
return allCategories;
624+
content.categoryIdentifier = newCategoryIdentifier;
635625
}
636626

637627
+ (void)addAttachments:(OSNotificationPayload*)payload
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Modified MIT License
3+
*
4+
* Copyright 2017 OneSignal
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* 1. The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* 2. All copies of substantial portions of the Software may only be used in connection
17+
* with services provided by OneSignal.
18+
*
19+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
* THE SOFTWARE.
26+
*/
27+
28+
#import <Foundation/Foundation.h>
29+
#import <UserNotifications/UserNotifications.h>
30+
31+
NS_ASSUME_NONNULL_BEGIN
32+
33+
34+
/**
35+
This class maintains a saved list of UNNotificationCategory ID
36+
strings. Allows the SDK to store unique UNNotificationCategory
37+
objects for each notification.
38+
39+
The SDK automatically prunes notification categories once more
40+
than MAX_CATEGORIES_SIZE categories have been registered.
41+
*/
42+
43+
@interface OneSignalNotificationCategoryController : NSObject
44+
45+
+ (OneSignalNotificationCategoryController *)sharedInstance;
46+
47+
- (NSString *)registerNotificationCategoryForNotificationId:(NSString *)notificationId;
48+
49+
- (NSMutableSet<UNNotificationCategory*>*)existingCategories;
50+
51+
@end
52+
53+
NS_ASSUME_NONNULL_END
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* Modified MIT License
3+
*
4+
* Copyright 2017 OneSignal
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* 1. The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* 2. All copies of substantial portions of the Software may only be used in connection
17+
* with services provided by OneSignal.
18+
*
19+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
* THE SOFTWARE.
26+
*/
27+
28+
#import "OneSignalNotificationCategoryController.h"
29+
#import "OneSignalExtensionBadgeHandler.h"
30+
#import "OneSignalHelper.h"
31+
#import "OneSignalCommonDefines.h"
32+
33+
#define CATEGORY_FORMAT_STRING(notificationId) [NSString stringWithFormat:@"__onesignal__dynamic__%@", notificationId]
34+
35+
@implementation OneSignalNotificationCategoryController
36+
37+
+ (OneSignalNotificationCategoryController *)sharedInstance {
38+
static OneSignalNotificationCategoryController *sharedInstance = nil;
39+
static dispatch_once_t once;
40+
dispatch_once(&once, ^{
41+
sharedInstance = [OneSignalNotificationCategoryController new];
42+
});
43+
return sharedInstance;
44+
}
45+
46+
// appends the new category ID to the current saved array of category ID's
47+
// The array is then inherently sorted in ascending order (the ID at index 0 is the oldest)
48+
// we want to run this on the main thread so that the extension service doesn't stop before it finishes
49+
// To prevent the SDK from registering too many categories as time goes by, we will prune the categories
50+
// when more than MAX_CATEGORIES_SIZE have been registered
51+
- (void)saveCategoryId:(NSString *)categoryId {
52+
let defaults = [[NSUserDefaults alloc] initWithSuiteName:OneSignalExtensionBadgeHandler.appGroupName];
53+
54+
NSMutableArray<NSString *> *mutableExisting = [self.existingRegisteredCategoryIds mutableCopy];
55+
56+
[mutableExisting addObject:categoryId];
57+
58+
// prune array if > max size
59+
if (mutableExisting.count > MAX_CATEGORIES_SIZE) {
60+
61+
// removes these categories from UNUserNotificationCenter
62+
[self pruneCategories:mutableExisting];
63+
64+
[mutableExisting removeObjectsInRange:NSMakeRange(0, mutableExisting.count - MAX_CATEGORIES_SIZE)];
65+
}
66+
67+
68+
[defaults setObject:mutableExisting forKey:SHARED_CATEGORY_LIST];
69+
70+
[defaults synchronize];
71+
}
72+
73+
- (NSArray<NSString *> *)existingRegisteredCategoryIds {
74+
let defaults = [[NSUserDefaults alloc] initWithSuiteName:OneSignalExtensionBadgeHandler.appGroupName];
75+
76+
NSArray<NSString *> *existing = [defaults arrayForKey:SHARED_CATEGORY_LIST] ?: [NSArray new];
77+
78+
return existing;
79+
}
80+
81+
- (void)pruneCategories:(NSMutableArray <NSString *> *)currentCategories {
82+
NSMutableSet<NSString *> *categoriesToRemove = [NSMutableSet new];
83+
84+
for (int i = (int)currentCategories.count - MAX_CATEGORIES_SIZE; i >= 0; i--)
85+
[categoriesToRemove addObject:currentCategories[i]];
86+
87+
let existingCategories = self.existingCategories;
88+
89+
NSMutableSet<UNNotificationCategory *> *newCategories = [NSMutableSet new];
90+
91+
for (UNNotificationCategory *category in existingCategories)
92+
if (![categoriesToRemove containsObject:category.identifier])
93+
[newCategories addObject:category];
94+
95+
[UNUserNotificationCenter.currentNotificationCenter setNotificationCategories:newCategories];
96+
}
97+
98+
- (NSString *)registerNotificationCategoryForNotificationId:(NSString *)notificationId {
99+
// if the notificationID is null/empty, just generate a random new UUID
100+
let categoryId = CATEGORY_FORMAT_STRING(notificationId ?: NSUUID.UUID.UUIDString);
101+
102+
[self saveCategoryId:categoryId];
103+
104+
return categoryId;
105+
}
106+
107+
- (NSMutableSet<UNNotificationCategory*>*)existingCategories {
108+
__block NSMutableSet* allCategories;
109+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
110+
let notificationCenter = [UNUserNotificationCenter currentNotificationCenter];
111+
[notificationCenter getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
112+
allCategories = [categories mutableCopy];
113+
dispatch_semaphore_signal(semaphore);
114+
}];
115+
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
116+
117+
return allCategories;
118+
}
119+
120+
@end

iOS_SDK/OneSignalSDK/UnitTests/UnitTestCommonMethods.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#import <Foundation/Foundation.h>
2929
#import <XCTest/XCTest.h>
3030
#import "OneSignal.h"
31+
#import "OneSignalNotificationCategoryController.h"
3132

3233
#define TEST_EXTERNAL_USER_ID @"i_am_a_test_external_user_id"
3334

@@ -53,6 +54,12 @@ NSString * serverUrlWithPath(NSString *path);
5354
+ (void)setDelayIntervals:(NSTimeInterval)apnsMaxWait withRegistrationDelay:(NSTimeInterval)registrationDelay;
5455
@end
5556

57+
// Expose methods on OneSignalNotificationCategoryController
58+
@interface OneSignalNotificationCategoryController ()
59+
- (void)pruneCategories:(NSMutableArray <NSString *> *)currentCategories;
60+
- (NSArray<NSString *> *)existingRegisteredCategoryIds;
61+
@end
62+
5663
// START - Start Observers
5764

5865
@interface OSPermissionStateTestObserver : NSObject<OSPermissionObserver> {

0 commit comments

Comments
 (0)