diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index cabab70a0..ad9d90b77 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -73,6 +73,8 @@ 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */; }; 3C2C7DC8288F3C020020F9AE /* OSSubscriptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */; }; 3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */; }; + 3C2DB2F12DE6CB5E0006B905 /* OneSignalBadgeHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C2DB2EF2DE6CB5E0006B905 /* OneSignalBadgeHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3C2DB2F22DE6CB5E0006B905 /* OneSignalBadgeHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C2DB2F02DE6CB5E0006B905 /* OneSignalBadgeHelpers.m */; }; 3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; }; 3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */ = {isa = PBXBuildFile; fileRef = 912411FC1E73342200E41FD7 /* OneSignalMobileProvision.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C448B9D2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */; }; @@ -1251,6 +1253,8 @@ 3C2C7DC2288E007E0020F9AE /* UnitTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UnitTests-Bridging-Header.h"; sourceTree = ""; }; 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSSubscriptionModel.swift; sourceTree = ""; }; 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDelta.swift; sourceTree = ""; }; + 3C2DB2EF2DE6CB5E0006B905 /* OneSignalBadgeHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalBadgeHelpers.h; sourceTree = ""; }; + 3C2DB2F02DE6CB5E0006B905 /* OneSignalBadgeHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalBadgeHelpers.m; sourceTree = ""; }; 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = ""; }; 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBackgroundTaskHandlerImpl.m; sourceTree = ""; }; 3C448BA12936B474002F96BC /* OSBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSBackgroundTaskManager.swift; sourceTree = ""; }; @@ -2596,6 +2600,8 @@ DE7D182C270273B0002D3A5D /* OSNotification.h */, 454F94F41FAD2E5A00D74CCF /* OSNotification.m */, 454F94F61FAD2EC300D74CCF /* OSNotification+Internal.h */, + 3C2DB2EF2DE6CB5E0006B905 /* OneSignalBadgeHelpers.h */, + 3C2DB2F02DE6CB5E0006B905 /* OneSignalBadgeHelpers.m */, 3CE8CC4C2911ADD1000DB0D3 /* OSDeviceUtils.h */, 3C47A972292642B100312125 /* OneSignalConfigManager.h */, 3C47A973292642B100312125 /* OneSignalConfigManager.m */, @@ -3138,6 +3144,7 @@ DE7D182F270275FF002D3A5D /* OneSignalTrackFirebaseAnalytics.h in Headers */, DEF7845C2912E89200A1F3A5 /* OSObservable.h in Headers */, DE7D1875270375FF002D3A5D /* OSReattemptRequest.h in Headers */, + 3C2DB2F12DE6CB5E0006B905 /* OneSignalBadgeHelpers.h in Headers */, DEF784652912FB2200A1F3A5 /* OSDialogInstanceManager.h in Headers */, DEF78493291479B200A1F3A5 /* OneSignalSelectorHelpers.h in Headers */, DE7D1862270374EE002D3A5D /* OSJSONHandling.h in Headers */, @@ -4427,6 +4434,7 @@ DEF784642912FA5100A1F3A5 /* OSDialogInstanceManager.m in Sources */, DE7D183B27027EFC002D3A5D /* NSURL+OneSignal.m in Sources */, DE7D186B270374EE002D3A5D /* OneSignalRequest.m in Sources */, + 3C2DB2F22DE6CB5E0006B905 /* OneSignalBadgeHelpers.m in Sources */, 3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */, 3CCF44BF299B17290021964D /* OneSignalWrapper.m in Sources */, DEF78492291479B200A1F3A5 /* OneSignalSelectorHelpers.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalBadgeHelpers.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalBadgeHelpers.h new file mode 100644 index 000000000..c3487b9f7 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalBadgeHelpers.h @@ -0,0 +1,32 @@ +/** + * Modified MIT License + * + * Copyright 2025 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 + +@interface OneSignalBadgeHelpers : NSObject ++ (void)updateCachedBadgeValue:(NSInteger)value usePreviousBadgeCount:(BOOL)usePrevious; +@end diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalBadgeHelpers.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalBadgeHelpers.m new file mode 100644 index 000000000..68eaada3d --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalBadgeHelpers.m @@ -0,0 +1,52 @@ +/** + * Modified MIT License + * + * Copyright 2025 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 "OneSignalBadgeHelpers.h" +#import "OneSignalUserDefaults.h" +#import "OneSignalCommonDefines.h" + +@implementation OneSignalBadgeHelpers + +/** + Store a previous badge count so that we can revert to this cached value when a notification display is cancelled. + When `usePreviousBadgeCount` is `true`, the `value` passed to this method will be unused. + */ ++ (void)updateCachedBadgeValue:(NSInteger)value usePreviousBadgeCount:(BOOL)usePrevious { + // Since badge logic can be executed in an extension, we need to use app groups to get + // a shared NSUserDefaults from the app group suite name + if (usePrevious) { + // Keep the PREVIOUS_ONESIGNAL_BADGE_KEY value unchanged + NSInteger previousBadgeCount = [OneSignalUserDefaults.initShared getSavedIntegerForKey:PREVIOUS_ONESIGNAL_BADGE_KEY defaultValue:0]; + [OneSignalUserDefaults.initShared saveIntegerForKey:ONESIGNAL_BADGE_KEY withValue:previousBadgeCount]; + } else { + NSInteger previousBadgeCount = [OneSignalUserDefaults.initShared getSavedIntegerForKey:ONESIGNAL_BADGE_KEY defaultValue:0]; + [OneSignalUserDefaults.initShared saveIntegerForKey:PREVIOUS_ONESIGNAL_BADGE_KEY withValue:previousBadgeCount]; + [OneSignalUserDefaults.initShared saveIntegerForKey:ONESIGNAL_BADGE_KEY withValue:value]; + } +} + +@end diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index bdcb97615..c6bf94c79 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -159,6 +159,8 @@ #define ONESIGNAL_DISABLE_BADGE_CLEARING @"OneSignal_disable_badge_clearing" #define ONESIGNAL_APP_GROUP_NAME_KEY @"OneSignal_app_groups_key" #define ONESIGNAL_BADGE_KEY @"onesignalBadgeCount" +/// Store the previous badge count to read for a cancelled notification display event +#define PREVIOUS_ONESIGNAL_BADGE_KEY @"previousOnesignalBadgeCount" // Firebase #define ONESIGNAL_FB_ENABLE_FIREBASE @"OS_ENABLE_FIREBASE_ANALYTICS" diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h index c7783d740..f3922e961 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h @@ -58,6 +58,7 @@ #import #import #import +#import // TODO: Testing: Should this class be defined in this file? @interface OneSignalCoreImpl : NSObject diff --git a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.h b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.h index f14ad763a..9311a7e01 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.h +++ b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.h @@ -31,6 +31,4 @@ @interface OneSignalExtensionBadgeHandler : NSObject + (void)handleBadgeCountWithNotificationRequest:(UNNotificationRequest *)request withNotification:(OSNotification *)notification withMutableNotificationContent:(UNMutableNotificationContent *)replacementContent; -+ (void)updateCachedBadgeValue:(NSInteger)value; -+ (NSInteger)currentCachedBadgeValue; @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.m b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.m index 0e5097b70..cfe1134a2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.m +++ b/iOS_SDK/OneSignalSDK/OneSignalExtension/OneSignalExtensionBadgeHandler.m @@ -35,7 +35,7 @@ + (void)handleBadgeCountWithNotificationRequest:(UNNotificationRequest *)request //make sure the OneSignal cached value is updated to this value if (!notification.badgeIncrement) { if (notification.hasBadge) - [OneSignalExtensionBadgeHandler updateCachedBadgeValue:notification.badge]; + [OneSignalBadgeHelpers updateCachedBadgeValue:notification.badge usePreviousBadgeCount:false]; return; } @@ -50,17 +50,11 @@ + (void)handleBadgeCountWithNotificationRequest:(UNNotificationRequest *)request replacementContent.badge = @(currentValue); - [OneSignalExtensionBadgeHandler updateCachedBadgeValue:currentValue]; + [OneSignalBadgeHelpers updateCachedBadgeValue:currentValue usePreviousBadgeCount:false]; } + (NSInteger)currentCachedBadgeValue { return [OneSignalUserDefaults.initShared getSavedIntegerForKey:ONESIGNAL_BADGE_KEY defaultValue:0]; } -+ (void)updateCachedBadgeValue:(NSInteger)value { - // Since badge logic can be executed in an extension, we need to use app groups to get - // a shared NSUserDefaults from the app group suite name - [OneSignalUserDefaults.initShared saveIntegerForKey:ONESIGNAL_BADGE_KEY withValue:value]; -} - @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotifications/Categories/UNUserNotificationCenter+OneSignalNotifications.m b/iOS_SDK/OneSignalSDK/OneSignalNotifications/Categories/UNUserNotificationCenter+OneSignalNotifications.m index 14bb1bbc1..e093bb469 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotifications/Categories/UNUserNotificationCenter+OneSignalNotifications.m +++ b/iOS_SDK/OneSignalSDK/OneSignalNotifications/Categories/UNUserNotificationCenter+OneSignalNotifications.m @@ -97,6 +97,12 @@ + (void)swizzleSelectors { [OneSignalNotificationsUNUserNotificationCenter class], @selector(onesignalGetNotificationSettingsWithCompletionHandler:) ); + injectSelector( + [UNUserNotificationCenter class], + @selector(setBadgeCount:withCompletionHandler:), + [OneSignalNotificationsUNUserNotificationCenter class], + @selector(onesignalSetBadgeCount:withCompletionHandler:) + ); } + (void)registerDelegate { @@ -167,6 +173,15 @@ - (void)onesignalGetNotificationSettingsWithCompletionHandler:(void(^)(UNNotific [self onesignalGetNotificationSettingsWithCompletionHandler:wrapperBlock]; } +/** + In order for the badge count to be consistent even in situations where the developer manually sets the badge number, + we swizzle the 'setBadgeCount()' method for ios 16+ to intercept these calls so we always know the latest count. This is especially + necessary as there is no equivalent "getBadgeCount" method available. + */ +- (void)onesignalSetBadgeCount:(NSInteger)badge withCompletionHandler:(void(^)(NSError *error))completionHandler { + [OneSignalBadgeHelpers updateCachedBadgeValue:badge usePreviousBadgeCount:false]; + [self onesignalSetBadgeCount:badge withCompletionHandler:completionHandler]; +} // A Set to keep track of which classes we have already swizzled so we only // swizzle each one once. If we swizzled more than once then this will create diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotification+OneSignal.m b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotification+OneSignal.m index f28970869..b2f9cefb1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotification+OneSignal.m +++ b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotification+OneSignal.m @@ -76,8 +76,8 @@ - (void)complete:(OSDisplayableNotification *)notification { reset the badge count to the value prior to receipt of this notif */ if (!notification) { - NSInteger previousBadgeCount = [UIApplication sharedApplication].applicationIconBadgeNumber; - [OneSignalUserDefaults.initShared saveIntegerForKey:ONESIGNAL_BADGE_KEY withValue:previousBadgeCount]; + // The badge count value of -99 will not be used, the previous badge count will be set + [OneSignalBadgeHelpers updateCachedBadgeValue:-99 usePreviousBadgeCount:true]; } if (_completion) { _completion(notification); diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.h b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.h index 81a5eaafb..a53d03d74 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.h +++ b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.h @@ -114,7 +114,7 @@ NS_SWIFT_NAME(onClick(event:)); + (void)handleWillShowInForegroundForNotification:(OSNotification *_Nonnull)notification completion:(OSNotificationDisplayResponse _Nonnull)completion; + (void)handleNotificationActionWithUrl:(NSString* _Nullable)url actionID:(NSString* _Nonnull)actionID; -+ (BOOL)clearBadgeCount:(BOOL)fromNotifOpened fromClearAll:(BOOL)fromClearAll; ++ (void)clearBadgeCount:(BOOL)fromNotifOpened fromClearAll:(BOOL)fromClearAll; + (BOOL)receiveRemoteNotification:(UIApplication* _Nonnull)application UserInfo:(NSDictionary* _Nonnull)userInfo completionHandler:(void (^_Nonnull)(UIBackgroundFetchResult))completionHandler; + (void)notificationReceived:(NSDictionary* _Nonnull)messageDict wasOpened:(BOOL)opened; diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m index b8cebda98..4a72345b6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m +++ b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m @@ -784,7 +784,7 @@ + (void)displayWebView:(NSURL*)url { openUrlBlock(true); } -+ (BOOL)clearBadgeCount:(BOOL)fromNotifOpened fromClearAll:(BOOL)fromClearAll { ++ (void)clearBadgeCount:(BOOL)fromNotifOpened fromClearAll:(BOOL)fromClearAll { NSNumber *disableBadgeNumber = [[NSBundle mainBundle] objectForInfoDictionaryKey:ONESIGNAL_DISABLE_BADGE_CLEARING]; @@ -794,20 +794,23 @@ + (BOOL)clearBadgeCount:(BOOL)fromNotifOpened fromClearAll:(BOOL)fromClearAll { _disableBadgeClearing = NO; if (_disableBadgeClearing && !fromClearAll) { - // The customer could have manually changed the badge value. We must ensure our cached value will match the current state. - [OneSignalUserDefaults.initShared saveIntegerForKey:ONESIGNAL_BADGE_KEY withValue:[UIApplication sharedApplication].applicationIconBadgeNumber]; - return false; + // The developer could have manually changed the badge value but we cannot read the current state. + // We swizzle badge count setters, which should be sufficient to ensure the cached value matches the current state. + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"clearBadgeCount called but badge clearing is disabled, not updating cached badge count"]; + return; } - bool wasBadgeSet = [UIApplication sharedApplication].applicationIconBadgeNumber > 0; - - if (fromNotifOpened || wasBadgeSet) { + if (@available(iOS 16.0, *)) { + [[UNUserNotificationCenter currentNotificationCenter] setBadgeCount:0 withCompletionHandler:^(NSError * _Nullable error) { + if (error) { + [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"clearBadgeCount encountered error setting badge count: %@", error]]; + } + }]; + } else { [OneSignalCoreHelper runOnMainThread:^{ [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; }]; } - - return wasBadgeSet; } + (BOOL)handleIAMPreview:(OSNotification *)notification { diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 6851e2167..10e7157ce 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -809,7 +809,7 @@ + (void)load { We swizzle the 'setApplicationIconBadgeNumber()' to intercept these calls so we always know the latest count */ - (void)onesignalSetApplicationIconBadgeNumber:(NSInteger)badge { - [OneSignalExtensionBadgeHandler updateCachedBadgeValue:badge]; + [OneSignalBadgeHelpers updateCachedBadgeValue:badge usePreviousBadgeCount:false]; [self onesignalSetApplicationIconBadgeNumber:badge]; }