From 057281328657dd9bba444c4f715f3398fd4caa09 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Mon, 17 Jun 2024 12:48:20 -0700 Subject: [PATCH 1/5] Clear badges when entering foreground NOT when didBecomeActive The application active state is different than the application foreground state. The app resigns and becomes active if the app is interrupted by the notification center even though it remains in the foreground. We previously were dismissing notifications in didBecomeActive so when the notification center was opened while the app was foregrounded we would dismiss notifications. There also seems to be an apple bug where the resign and become active triggers would fire twice as you are swiping down the notification center. This means that we were dismissing the notifications as you open the notification center which made the problem even worse. --- .../Source/OneSignalLifecycleObserver.m | 6 ++++++ iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h | 1 + iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m | 14 ++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m b/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m index fab07c036..c1eff394b 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m @@ -68,6 +68,7 @@ + (void)registerLifecycleObserverAsUIScene { + (void)registerLifecycleObserverAsUIApplication { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"registering for Application Lifecycle notifications"]; [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(didEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(willEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(didBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(willResignActive) name:UIApplicationWillResignActiveNotification object:nil]; } @@ -112,6 +113,11 @@ - (void)didEnterBackground { } } +- (void)willEnterForeground { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"application/scene willEnterForeground"]; + [OneSignalTracker applicationWillEnterForeground]; +} + - (void)dealloc { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"lifecycle observer deallocated"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h index 2c7520658..4c6da5e0b 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h @@ -31,5 +31,6 @@ + (void)onFocus:(BOOL)toBackground; + (void)onSessionEnded:(NSArray *) lastInfluences; ++ (void)applicationWillEnterForeground; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m index 453186ed4..42895a458 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m @@ -77,12 +77,20 @@ + (void)onFocus:(BOOL)toBackground { if (toBackground) { [self applicationBackgrounded]; } else { - [self applicationForegrounded]; + [self applicationBecameActive]; } } -+ (void)applicationForegrounded { +// This is a separate lifecycle event than application became active +// Notably this is NOT called when the app resumes after resigning active +// From things like entering and exiting the notification center ++ (void)applicationWillEnterForeground { [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"Application Foregrounded started"]; + [OSNotificationsManager clearBadgeCount:false fromClearAll:false]; +} + ++ (void)applicationBecameActive { + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"Application Active started"]; [OSFocusTimeProcessorFactory cancelFocusCall]; if (OSSessionManager.sharedSessionManager.appEntryState != NOTIFICATION_CLICK) @@ -100,8 +108,6 @@ + (void)applicationForegrounded { // TODO: Here it used to call receivedInAppMessageJson with nil, this method no longer exists // [OneSignal receivedInAppMessageJson:nil]; } - - [OSNotificationsManager clearBadgeCount:false fromClearAll:false]; } + (void)applicationBackgrounded { From cb1d4613e227061c3ec57fef30826c435313ced0 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Thu, 20 Jun 2024 14:14:16 -0700 Subject: [PATCH 2/5] move badge clearing and notif type updates to notifications module Move uiscene check to core in a bundle utils file Create stub unit testing file for notifications --- .../OneSignal.xcodeproj/project.pbxproj | 231 +++++++++++++++++- .../OneSignalCore/Source/OSBundleUtils.h | 11 + .../OneSignalCore/Source/OSBundleUtils.m | 20 ++ .../OneSignalCore/Source/OneSignalCore.h | 2 +- .../OSNotificationsManager.m | 33 +++ .../OneSignalNotificationsTests.swift | 72 ++++++ .../Source/OneSignalLifecycleObserver.m | 8 +- .../OneSignalSDK/Source/OneSignalTracker.h | 1 - .../OneSignalSDK/Source/OneSignalTracker.m | 10 - .../Source/UIApplication+OneSignal.h | 1 - .../Source/UIApplication+OneSignal.m | 10 +- 11 files changed, 370 insertions(+), 29 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h create mode 100644 iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m create mode 100644 iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 53e34d17e..4921ad252 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -469,6 +469,10 @@ DEA98C1928C90EE5000C6856 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; DEA98C1C28C90EE6000C6856 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C115161289A259500565C41 /* OneSignalOSCore.framework */; }; DEA98C1E28C90EE9000C6856 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; + DEBA2A1D2C20E35E00E234DB /* OneSignalNotificationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBA2A1C2C20E35E00E234DB /* OneSignalNotificationsTests.swift */; }; + DEBA2A1E2C20E35E00E234DB /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEF784292912DEB600A1F3A5 /* OneSignalNotifications.framework */; }; + DEBA2A262C20E9AA00E234DB /* OSBundleUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DEBA2A252C20E9AA00E234DB /* OSBundleUtils.m */; }; + DEBA2A282C24D0F400E234DB /* OSBundleUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBA2A272C24D0ED00E234DB /* OSBundleUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; DEBAADFC2A420A3900BF2C1C /* OneSignalLocationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBAADFB2A420A3900BF2C1C /* OneSignalLocationManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; DEBAAE042A420C9800BF2C1C /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; DEBAAE0A2A420CA500BF2C1C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEBAAE092A420CA500BF2C1C /* UIKit.framework */; }; @@ -880,6 +884,13 @@ remoteGlobalIDString = 3CC063992B6D7A8C002BB07F; remoteInfo = OneSignalCoreMocks; }; + DEBA2A1F2C20E35E00E234DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEF784282912DEB600A1F3A5; + remoteInfo = OneSignalNotifications; + }; DEBAAE062A420C9800BF2C1C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 37747F8B19147D6400558FAD /* Project object */; @@ -1460,6 +1471,10 @@ DEB843A127C0245B00D7E943 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DEB843A327C0246A00D7E943 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DEB843A527C0247700D7E943 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DEBA2A1A2C20E35E00E234DB /* OneSignalNotificationsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalNotificationsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DEBA2A1C2C20E35E00E234DB /* OneSignalNotificationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalNotificationsTests.swift; sourceTree = ""; }; + DEBA2A252C20E9AA00E234DB /* OSBundleUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBundleUtils.m; sourceTree = ""; }; + DEBA2A272C24D0ED00E234DB /* OSBundleUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBundleUtils.h; sourceTree = ""; }; DEBAADF92A420A3700BF2C1C /* OneSignalLocation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DEBAADFB2A420A3900BF2C1C /* OneSignalLocationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalLocationManager.h; sourceTree = ""; }; DEBAAE022A420B8000BF2C1C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1721,6 +1736,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DEBA2A172C20E35E00E234DB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DEBA2A1E2C20E35E00E234DB /* OneSignalNotifications.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DEBAADF62A420A3700BF2C1C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1807,6 +1830,7 @@ 37747F8A19147D6400558FAD = { isa = PBXGroup; children = ( + DEBA2A1B2C20E35E00E234DB /* OneSignalNotificationsTests */, 37747F9519147D6500558FAD /* Frameworks */, 3E2400391D4FFC31008BDE70 /* OneSignalFramework */, DEBAAE212A42119100BF2C1C /* OneSignalInAppMessagesFramework */, @@ -1860,6 +1884,7 @@ 3CC063EB2B6D7FE8002BB07F /* OneSignalUserTests.xctest */, 475F471E2B8E398D00EC05B3 /* OneSignalLiveActivities.framework */, 4735424A2B8F93330016DB4C /* OneSignalLiveActivitiesTests.xctest */, + DEBA2A1A2C20E35E00E234DB /* OneSignalNotificationsTests.xctest */, ); name = Products; sourceTree = ""; @@ -2379,6 +2404,8 @@ DEBAAEAF2A435AE900BF2C1C /* OSInAppMessages.h */, DEBAAEB22A436CE800BF2C1C /* OSStubInAppMessages.m */, DEBAAEB42A436D5D00BF2C1C /* OSStubLocation.m */, + DEBA2A272C24D0ED00E234DB /* OSBundleUtils.h */, + DEBA2A252C20E9AA00E234DB /* OSBundleUtils.m */, ); path = Source; sourceTree = ""; @@ -2601,6 +2628,14 @@ path = OneSignalOSCoreFramework; sourceTree = ""; }; + DEBA2A1B2C20E35E00E234DB /* OneSignalNotificationsTests */ = { + isa = PBXGroup; + children = ( + DEBA2A1C2C20E35E00E234DB /* OneSignalNotificationsTests.swift */, + ); + path = OneSignalNotificationsTests; + sourceTree = ""; + }; DEBAADFA2A420A3900BF2C1C /* OneSignalLocation */ = { isa = PBXGroup; children = ( @@ -2876,6 +2911,7 @@ DE7D1868270374EE002D3A5D /* OneSignalRequest.h in Headers */, DE7D183027027973002D3A5D /* OSNotification+Internal.h in Headers */, DE7D183D27027F13002D3A5D /* NSURL+OneSignal.h in Headers */, + DEBA2A282C24D0F400E234DB /* OSBundleUtils.h in Headers */, DEF784792914667A00A1F3A5 /* NSDateFormatter+OneSignal.h in Headers */, DE7D17EB27026B95002D3A5D /* OneSignalCore.h in Headers */, DE7D182A270271A9002D3A5D /* OneSignalCommonDefines.h in Headers */, @@ -3286,6 +3322,24 @@ productReference = DE7D188027037F43002D3A5D /* OneSignalOutcomes.framework */; productType = "com.apple.product-type.framework"; }; + DEBA2A192C20E35E00E234DB /* OneSignalNotificationsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DEBA2A242C20E35E00E234DB /* Build configuration list for PBXNativeTarget "OneSignalNotificationsTests" */; + buildPhases = ( + DEBA2A162C20E35E00E234DB /* Sources */, + DEBA2A172C20E35E00E234DB /* Frameworks */, + DEBA2A182C20E35E00E234DB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DEBA2A202C20E35E00E234DB /* PBXTargetDependency */, + ); + name = OneSignalNotificationsTests; + productName = OneSignalNotificationsTests; + productReference = DEBA2A1A2C20E35E00E234DB /* OneSignalNotificationsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; DEBAADF82A420A3700BF2C1C /* OneSignalLocation */ = { isa = PBXNativeTarget; buildConfigurationList = DEBAAE002A420A3A00BF2C1C /* Build configuration list for PBXNativeTarget "OneSignalLocation" */; @@ -3381,7 +3435,7 @@ 37747F8B19147D6400558FAD /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 0800; ORGANIZATIONNAME = Hiptic; TargetAttributes = { @@ -3474,6 +3528,9 @@ DevelopmentTeam = 99SW8E36CT; ProvisioningStyle = Automatic; }; + DEBA2A192C20E35E00E234DB = { + CreatedOnToolsVersion = 16.0; + }; DEBAADF82A420A3700BF2C1C = { CreatedOnToolsVersion = 14.3; DevelopmentTeam = 99SW8E36CT; @@ -3536,6 +3593,7 @@ 3CC063A02B6D7A8D002BB07F /* OneSignalCoreTests */, 3CC063EA2B6D7FE8002BB07F /* OneSignalUserTests */, 473542492B8F93330016DB4C /* OneSignalLiveActivitiesTests */, + DEBA2A192C20E35E00E234DB /* OneSignalNotificationsTests */, ); }; /* End PBXProject section */ @@ -3639,6 +3697,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DEBA2A182C20E35E00E234DB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DEBAADF72A420A3700BF2C1C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3987,6 +4052,7 @@ buildActionMask = 2147483647; files = ( DEBAAEB32A436CE800BF2C1C /* OSStubInAppMessages.m in Sources */, + DEBA2A262C20E9AA00E234DB /* OSBundleUtils.m in Sources */, 3C47A975292642B100312125 /* OneSignalConfigManager.m in Sources */, DE7D1874270375FF002D3A5D /* OSReattemptRequest.m in Sources */, DE7D183427027A73002D3A5D /* OneSignalLog.m in Sources */, @@ -4063,6 +4129,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DEBA2A162C20E35E00E234DB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DEBA2A1D2C20E35E00E234DB /* OneSignalNotificationsTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DEBAADF52A420A3700BF2C1C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4341,6 +4415,11 @@ target = 3CC063992B6D7A8C002BB07F /* OneSignalCoreMocks */; targetProxy = DEA69F472C190045009BB128 /* PBXContainerItemProxy */; }; + DEBA2A202C20E35E00E234DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEF784282912DEB600A1F3A5 /* OneSignalNotifications */; + targetProxy = DEBA2A1F2C20E35E00E234DB /* PBXContainerItemProxy */; + }; DEBAAE072A420C9800BF2C1C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DE7D17E527026B95002D3A5D /* OneSignalCore */; @@ -6979,6 +7058,146 @@ }; name = Debug; }; + DEBA2A212C20E35E00E234DB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 99SW8E36CT; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalNotificationsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + DEBA2A222C20E35E00E234DB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 99SW8E36CT; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalNotificationsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DEBA2A232C20E35E00E234DB /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 99SW8E36CT; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.OneSignalNotificationsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Test; + }; DEBAADFD2A420A3A00BF2C1C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -7881,6 +8100,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DEBA2A242C20E35E00E234DB /* Build configuration list for PBXNativeTarget "OneSignalNotificationsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DEBA2A212C20E35E00E234DB /* Release */, + DEBA2A222C20E35E00E234DB /* Debug */, + DEBA2A232C20E35E00E234DB /* Test */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DEBAAE002A420A3A00BF2C1C /* Build configuration list for PBXNativeTarget "OneSignalLocation" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h new file mode 100644 index 000000000..a00b07de3 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h @@ -0,0 +1,11 @@ +// +// OSBundleUtils.h +// OneSignal +// +// Created by Elliot Mawby on 6/20/24. +// Copyright © 2024 Hiptic. All rights reserved. +// + +@interface OSBundleUtils : NSObject ++ (BOOL)isAppUsingUIScene; +@end diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m new file mode 100644 index 000000000..45bf5090c --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m @@ -0,0 +1,20 @@ +// +// OSBundleUtils.m +// OneSignalCore +// +// Created by Elliot Mawby on 6/17/24. +// Copyright © 2024 Hiptic. All rights reserved. +// + +#import +#import "OSBundleUtils.h" +@implementation OSBundleUtils + ++ (BOOL)isAppUsingUIScene { + if (@available(iOS 13.0, *)) { + return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"] != nil; + } + return NO; +} + +@end diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h index 12a95335c..72843f933 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCore.h @@ -56,7 +56,7 @@ #import #import #import - +#import // TODO: Testing: Should this class be defined in this file? @interface OneSignalCoreImpl : NSObject diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m index 4ca2f454a..9ce51e29b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m +++ b/iOS_SDK/OneSignalSDK/OneSignalNotifications/OSNotificationsManager.m @@ -251,9 +251,38 @@ + (void)start { @selector(onesignalSetApplicationIconBadgeNumber:) ); [OneSignalNotificationsUNUserNotificationCenter setup]; + + [self registerLifecycleObserver]; + } #pragma clang diagnostic pop ++ (void)registerLifecycleObserver { + // Replacing swizzled lifecycle selectors with notification center observers for scene based Apps + if ([OSBundleUtils isAppUsingUIScene]) { + [self registerLifecycleObserverAsUIScene]; + } else { + [self registerLifecycleObserverAsUIApplication]; + } +} + ++ (void)registerLifecycleObserverAsUIScene { + if (@available(iOS 13.0, *)) { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"OSNotificationManager registering for Scene Lifecycle notifications"]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground) name:@"UISceneWillEnterForegroundNotification" object:nil]; + } +} + ++ (void)registerLifecycleObserverAsUIApplication { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"OSNotificationManager registering for Application Lifecycle notifications"]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; +} + ++ (void)willEnterForeground { + [OSNotificationsManager clearBadgeCount:false fromClearAll:false]; + [OSNotificationsManager sendNotificationTypesUpdateToDelegate]; +} + + (void)resetLocals { _lastMessageReceived = nil; _lastMessageIdFromAction = nil; @@ -967,6 +996,10 @@ + (UNNotificationRequest*)prepareUNNotificationRequest:(OSNotification*)notifica return [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger]; } +- (void)dealloc { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"OSNotificationsManager observer deallocated"]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift new file mode 100644 index 000000000..7c88ec386 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift @@ -0,0 +1,72 @@ +// +// OneSignalNotificationsTests.swift +// OneSignalNotificationsTests +// +// Created by Elliot Mawby on 6/17/24. +// Copyright © 2024 Hiptic. All rights reserved. +// + +import XCTest + +final class OneSignalNotificationsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + + func testClearBadgesWhenAppEntersForeground() throws { + // NotificationManager Start? Or Mock NotificationManager start + // Mock receive a notification or have badge count > 0 + + // Then background the app + + // Foreground the app + + // Ensure that badge count == 0 + } + + func testDontclearBadgesWhenAppBecomesActive() throws { + // NotificationManager Start? Or Mock NotificationManager start + // Mock receive a notification or have badge count > 0 + + // Then resign active + + // App becomes active the app + + // Ensure that badge count == previous badge count + } + + func testUpdateNotificationTypesOnAppEntersForeground() throws { + // NotificationManager Start? Or Mock NotificationManager start + // Deny notification permission + + // Then background the app + + // Change app notification permissions + + // Foreground the app for within 30 seconds + + // Ensure that we update the notification types + } + + +} diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m b/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m index c1eff394b..0e1389a2b 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalLifecycleObserver.m @@ -49,7 +49,7 @@ +(OneSignalLifecycleObserver*) sharedInstance { + (void)registerLifecycleObserver { // Replacing swizzled lifecycle selectors with notification center observers for scene based Apps - if ([UIApplication isAppUsingUIScene]) { + if ([OSBundleUtils isAppUsingUIScene]) { [self registerLifecycleObserverAsUIScene]; } else { [self registerLifecycleObserverAsUIApplication]; @@ -68,7 +68,6 @@ + (void)registerLifecycleObserverAsUIScene { + (void)registerLifecycleObserverAsUIApplication { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"registering for Application Lifecycle notifications"]; [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(didEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(willEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(didBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:[OneSignalLifecycleObserver sharedInstance] selector:@selector(willResignActive) name:UIApplicationWillResignActiveNotification object:nil]; } @@ -113,11 +112,6 @@ - (void)didEnterBackground { } } -- (void)willEnterForeground { - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"application/scene willEnterForeground"]; - [OneSignalTracker applicationWillEnterForeground]; -} - - (void)dealloc { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"lifecycle observer deallocated"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h index 4c6da5e0b..2c7520658 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.h @@ -31,6 +31,5 @@ + (void)onFocus:(BOOL)toBackground; + (void)onSessionEnded:(NSArray *) lastInfluences; -+ (void)applicationWillEnterForeground; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m index 42895a458..7c77d1d94 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalTracker.m @@ -81,14 +81,6 @@ + (void)onFocus:(BOOL)toBackground { } } -// This is a separate lifecycle event than application became active -// Notably this is NOT called when the app resumes after resigning active -// From things like entering and exiting the notification center -+ (void)applicationWillEnterForeground { - [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"Application Foregrounded started"]; - [OSNotificationsManager clearBadgeCount:false fromClearAll:false]; -} - + (void)applicationBecameActive { [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:@"Application Active started"]; [OSFocusTimeProcessorFactory cancelFocusCall]; @@ -102,8 +94,6 @@ + (void)applicationBecameActive { if ([OneSignal shouldStartNewSession]) [OneSignal startNewSession:NO]; else { - // This checks if notification permissions changed when app was backgrounded - [OSNotificationsManager sendNotificationTypesUpdateToDelegate]; [[OSSessionManager sharedSessionManager] attemptSessionUpgrade]; // TODO: Here it used to call receivedInAppMessageJson with nil, this method no longer exists // [OneSignal receivedInAppMessageJson:nil]; diff --git a/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.h b/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.h index 89e77c3c7..b1ff274ea 100644 --- a/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.h +++ b/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.h @@ -28,5 +28,4 @@ #import @interface UIApplication (OneSignal) + (BOOL)applicationIsActive; -+ (BOOL)isAppUsingUIScene; @end diff --git a/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.m b/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.m index e37f0e4c2..d48950e0d 100644 --- a/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/UIApplication+OneSignal.m @@ -27,11 +27,12 @@ #import "UIApplication+OneSignal.h" #import +#import @implementation UIApplication (OneSignal) + (BOOL)applicationIsActive { - if ([self isAppUsingUIScene] && [NSThread isMainThread]) { + if ([OSBundleUtils isAppUsingUIScene] && [NSThread isMainThread]) { if (@available(iOS 13.0, *)) { UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow; id windowScene = [keyWindow performSelector:@selector(windowScene)]; @@ -43,11 +44,4 @@ + (BOOL)applicationIsActive { return [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; } -+ (BOOL)isAppUsingUIScene { - if (@available(iOS 13.0, *)) { - return [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"] != nil; - } - return NO; -} - @end From 14beee9e76035aa921c111c2025ada912e3260d4 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Thu, 20 Jun 2024 14:51:25 -0700 Subject: [PATCH 3/5] add OneSignalNotificationsTests to unittestapp schemes --- .../OneSignal.xcodeproj/project.pbxproj | 56 ++++++++++++++++++- .../xcschemes/UnitTestApp.xcscheme | 31 ++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 4921ad252..11a35d325 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -473,6 +473,8 @@ DEBA2A1E2C20E35E00E234DB /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEF784292912DEB600A1F3A5 /* OneSignalNotifications.framework */; }; DEBA2A262C20E9AA00E234DB /* OSBundleUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DEBA2A252C20E9AA00E234DB /* OSBundleUtils.m */; }; DEBA2A282C24D0F400E234DB /* OSBundleUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBA2A272C24D0ED00E234DB /* OSBundleUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DEBA2A2B2C24DA5800E234DB /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; + DEBA2A302C24DA5C00E234DB /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; }; DEBAADFC2A420A3900BF2C1C /* OneSignalLocationManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DEBAADFB2A420A3900BF2C1C /* OneSignalLocationManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; DEBAAE042A420C9800BF2C1C /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; DEBAAE0A2A420CA500BF2C1C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DEBAAE092A420CA500BF2C1C /* UIKit.framework */; }; @@ -891,6 +893,27 @@ remoteGlobalIDString = DEF784282912DEB600A1F3A5; remoteInfo = OneSignalNotifications; }; + DEBA2A2D2C24DA5800E234DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = DE7D17E527026B95002D3A5D; + remoteInfo = OneSignalCore; + }; + DEBA2A322C24DA5C00E234DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3CC063992B6D7A8C002BB07F; + remoteInfo = OneSignalCoreMocks; + }; + DEBA2A342C24DB2B00E234DB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 37747F8B19147D6400558FAD /* Project object */; + proxyType = 1; + remoteGlobalIDString = DEF5CCF02539321A0003E9CC; + remoteInfo = UnitTestApp; + }; DEBAAE062A420C9800BF2C1C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 37747F8B19147D6400558FAD /* Project object */; @@ -1740,6 +1763,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DEBA2A302C24DA5C00E234DB /* OneSignalCoreMocks.framework in Frameworks */, + DEBA2A2B2C24DA5800E234DB /* OneSignalCore.framework in Frameworks */, DEBA2A1E2C20E35E00E234DB /* OneSignalNotifications.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3334,6 +3359,9 @@ ); dependencies = ( DEBA2A202C20E35E00E234DB /* PBXTargetDependency */, + DEBA2A2E2C24DA5800E234DB /* PBXTargetDependency */, + DEBA2A332C24DA5C00E234DB /* PBXTargetDependency */, + DEBA2A352C24DB2B00E234DB /* PBXTargetDependency */, ); name = OneSignalNotificationsTests; productName = OneSignalNotificationsTests; @@ -3530,6 +3558,7 @@ }; DEBA2A192C20E35E00E234DB = { CreatedOnToolsVersion = 16.0; + TestTargetID = DEF5CCF02539321A0003E9CC; }; DEBAADF82A420A3700BF2C1C = { CreatedOnToolsVersion = 14.3; @@ -4420,6 +4449,21 @@ target = DEF784282912DEB600A1F3A5 /* OneSignalNotifications */; targetProxy = DEBA2A1F2C20E35E00E234DB /* PBXContainerItemProxy */; }; + DEBA2A2E2C24DA5800E234DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DE7D17E527026B95002D3A5D /* OneSignalCore */; + targetProxy = DEBA2A2D2C24DA5800E234DB /* PBXContainerItemProxy */; + }; + DEBA2A332C24DA5C00E234DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3CC063992B6D7A8C002BB07F /* OneSignalCoreMocks */; + targetProxy = DEBA2A322C24DA5C00E234DB /* PBXContainerItemProxy */; + }; + DEBA2A352C24DB2B00E234DB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DEF5CCF02539321A0003E9CC /* UnitTestApp */; + targetProxy = DEBA2A342C24DB2B00E234DB /* PBXContainerItemProxy */; + }; DEBAAE072A420C9800BF2C1C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DE7D17E527026B95002D3A5D /* OneSignalCore */; @@ -7062,6 +7106,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -7088,7 +7133,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; @@ -7099,6 +7144,7 @@ SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; }; name = Release; }; @@ -7106,6 +7152,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -7138,7 +7185,7 @@ "$(inherited)", ); GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -7151,6 +7198,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; }; name = Debug; }; @@ -7158,6 +7206,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -7184,7 +7233,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; @@ -7195,6 +7244,7 @@ SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UnitTestApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/UnitTestApp"; }; name = Test; }; diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme index 0b8d3c4ea..94f60e4de 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/xcshareddata/xcschemes/UnitTestApp.xcscheme @@ -48,6 +48,20 @@ ReferencedContainer = "container:OneSignal.xcodeproj"> + + + + + + + + + + Date: Thu, 20 Jun 2024 15:19:47 -0700 Subject: [PATCH 4/5] Add notifications unit tests for foregrounding the app --- .../OneSignal.xcodeproj/project.pbxproj | 8 ++ .../OneSignalCoreMocks.swift | 44 +++++++++++ .../OneSignalNotificationsTests.swift | 74 +++++++++++-------- 3 files changed, 95 insertions(+), 31 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 11a35d325..7502651c7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -454,6 +454,8 @@ DE971754274C48CF00FC409E /* OSPrivacyConsentController.h in Headers */ = {isa = PBXBuildFile; fileRef = DE971753274C48CF00FC409E /* OSPrivacyConsentController.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE9717642756BCFB00FC409E /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17F927026BA3002D3A5D /* OneSignalExtension.framework */; }; DE9717662756BCFD00FC409E /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; + DE9776FF2C24DFE800ACB25F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE9776FE2C24DFE800ACB25F /* UIKit.framework */; }; + DE9777012C24DFF300ACB25F /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE9777002C24DFF300ACB25F /* UserNotifications.framework */; }; DEA4B44E2888AF8900E9FE12 /* OneSignalUNUserNotificationCenterOverrider.m in Sources */ = {isa = PBXBuildFile; fileRef = DEA4B44C2888AF8900E9FE12 /* OneSignalUNUserNotificationCenterOverrider.m */; }; DEA4B45A2888BFAB00E9FE12 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; DEA4B45C2888C1D000E9FE12 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17E627026B95002D3A5D /* OneSignalCore.framework */; }; @@ -1485,6 +1487,8 @@ DE7D18DE2703B49B002D3A5D /* OSFocusRequests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSFocusRequests.m; sourceTree = ""; }; DE971751274C48B700FC409E /* OSPrivacyConsentController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSPrivacyConsentController.m; sourceTree = ""; }; DE971753274C48CF00FC409E /* OSPrivacyConsentController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSPrivacyConsentController.h; sourceTree = ""; }; + DE9776FE2C24DFE800ACB25F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/iOSSupport/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + DE9777002C24DFF300ACB25F /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.0.sdk/System/Library/Frameworks/UserNotifications.framework; sourceTree = DEVELOPER_DIR; }; DE9877292591654600DE07D5 /* NSDateFormatter+OneSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDateFormatter+OneSignal.h"; sourceTree = ""; }; DE98772A2591655800DE07D5 /* NSDateFormatter+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDateFormatter+OneSignal.m"; sourceTree = ""; }; DEA4B44C2888AF8900E9FE12 /* OneSignalUNUserNotificationCenterOverrider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OneSignalUNUserNotificationCenterOverrider.m; sourceTree = ""; }; @@ -1763,7 +1767,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + DE9777012C24DFF300ACB25F /* UserNotifications.framework in Frameworks */, DEBA2A302C24DA5C00E234DB /* OneSignalCoreMocks.framework in Frameworks */, + DE9776FF2C24DFE800ACB25F /* UIKit.framework in Frameworks */, DEBA2A2B2C24DA5800E234DB /* OneSignalCore.framework in Frameworks */, DEBA2A1E2C20E35E00E234DB /* OneSignalNotifications.framework in Frameworks */, ); @@ -1917,6 +1923,8 @@ 37747F9519147D6500558FAD /* Frameworks */ = { isa = PBXGroup; children = ( + DE9777002C24DFF300ACB25F /* UserNotifications.framework */, + DE9776FE2C24DFE800ACB25F /* UIKit.framework */, DEFB3E622BB731BD00E65DAD /* ActivityKit.framework */, 3C7A39D42B7C18EE0082665E /* XCTest.framework */, DEBAAE9A2A4222B000BF2C1C /* CoreGraphics.framework */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift index 757b53d93..4e452adeb 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift @@ -51,4 +51,48 @@ public class OneSignalCoreMocks: NSObject { let expectation = XCTestExpectation(description: "Wait for \(seconds) seconds") _ = XCTWaiter.wait(for: [expectation], timeout: seconds) } + + @objc public static func backgroundApp() { + if (OSBundleUtils.isAppUsingUIScene()) { + if #available(iOS 13.0, *) { + NotificationCenter.default.post(name: UIScene.willDeactivateNotification, object: nil) + NotificationCenter.default.post(name: UIScene.didEnterBackgroundNotification, object: nil) + } + } else { + NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) + } + } + + @objc public static func foregroundApp() { + if (OSBundleUtils.isAppUsingUIScene()) { + if #available(iOS 13.0, *) { + NotificationCenter.default.post(name: UIScene.willEnterForegroundNotification, object: nil) + NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil) + } + } else { + NotificationCenter.default.post(name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + } + } + + @objc public static func resignActive() { + if (OSBundleUtils.isAppUsingUIScene()) { + if #available(iOS 13.0, *) { + NotificationCenter.default.post(name: UIScene.willDeactivateNotification, object: nil) + } + } else { + NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil) + } + } + + @objc public static func becomeActive() { + if (OSBundleUtils.isAppUsingUIScene()) { + if #available(iOS 13.0, *) { + NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil) + } + } else { + NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil) + } + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift index 7c88ec386..e452ae22e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift @@ -7,66 +7,78 @@ // import XCTest +import OneSignalNotifications +import OneSignalCoreMocks +import UIKit final class OneSignalNotificationsTests: XCTestCase { + + var notifTypes: Int32 = 0 + var token: String = "" override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. + self.notifTypes = 0 + self.token = "" } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } - func testClearBadgesWhenAppEntersForeground() throws { - // NotificationManager Start? Or Mock NotificationManager start - // Mock receive a notification or have badge count > 0 - + // NotificationManager Start to register lifecycle listener + OSNotificationsManager.start() + // Set badge count > 0 + UIApplication.shared.applicationIconBadgeNumber = 1 // Then background the app - + OneSignalCoreMocks.backgroundApp() // Foreground the app - + OneSignalCoreMocks.foregroundApp() // Ensure that badge count == 0 + XCTAssertEqual(UIApplication.shared.applicationIconBadgeNumber, 0) } func testDontclearBadgesWhenAppBecomesActive() throws { - // NotificationManager Start? Or Mock NotificationManager start - // Mock receive a notification or have badge count > 0 - + // NotificationManager Start to register lifecycle listener + OSNotificationsManager.start() + // Set badge count > 0 + UIApplication.shared.applicationIconBadgeNumber = 1 // Then resign active - + OneSignalCoreMocks.resignActive() // App becomes active the app - - // Ensure that badge count == previous badge count + OneSignalCoreMocks.becomeActive() + // Ensure that badge count == 0 + XCTAssertEqual(UIApplication.shared.applicationIconBadgeNumber, 1) } func testUpdateNotificationTypesOnAppEntersForeground() throws { - // NotificationManager Start? Or Mock NotificationManager start - // Deny notification permission + // NotificationManager Start to register lifecycle listener + OSNotificationsManager.start() - // Then background the app + OSNotificationsManager.delegate = self - // Change app notification permissions + XCTAssertEqual(self.notifTypes, 0) + + // Then background the app + OneSignalCoreMocks.backgroundApp() // Foreground the app for within 30 seconds + OneSignalCoreMocks.foregroundApp() - // Ensure that we update the notification types + // Ensure that the delegate is updated with the new notification type + XCTAssertEqual(self.notifTypes, ERROR_PUSH_NEVER_PROMPTED) } } + +extension OneSignalNotificationsTests: OneSignalNotificationsDelegate { + public func setNotificationTypes(_ notificationTypes: Int32) { + self.notifTypes = notificationTypes + } + + public func setPushToken(_ pushToken: String) { + self.token = pushToken + } +} From bb60c99d1427013d05a992eeace6f4213608eb51 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 26 Jun 2024 12:07:41 -0700 Subject: [PATCH 5/5] cleanup comments for notifications tests and bundle utils --- .../OneSignalCore/Source/OSBundleUtils.h | 28 ++++++++++++----- .../OneSignalCore/Source/OSBundleUtils.m | 28 ++++++++++++----- .../OneSignalNotificationsTests.swift | 30 ++++++++++++++----- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h index a00b07de3..d86f1940f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.h @@ -1,10 +1,24 @@ -// -// OSBundleUtils.h -// OneSignal -// -// Created by Elliot Mawby on 6/20/24. -// Copyright © 2024 Hiptic. All rights reserved. -// +/* + Modified MIT License + Copyright 2024 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. + */ @interface OSBundleUtils : NSObject + (BOOL)isAppUsingUIScene; diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m index 45bf5090c..dfd407799 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OSBundleUtils.m @@ -1,10 +1,24 @@ -// -// OSBundleUtils.m -// OneSignalCore -// -// Created by Elliot Mawby on 6/17/24. -// Copyright © 2024 Hiptic. All rights reserved. -// +/* + Modified MIT License + Copyright 2024 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 #import "OSBundleUtils.h" diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift index e452ae22e..69df25803 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift @@ -1,10 +1,24 @@ -// -// OneSignalNotificationsTests.swift -// OneSignalNotificationsTests -// -// Created by Elliot Mawby on 6/17/24. -// Copyright © 2024 Hiptic. All rights reserved. -// +/* + Modified MIT License + Copyright 2024 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 XCTest import OneSignalNotifications @@ -48,7 +62,7 @@ final class OneSignalNotificationsTests: XCTestCase { OneSignalCoreMocks.resignActive() // App becomes active the app OneSignalCoreMocks.becomeActive() - // Ensure that badge count == 0 + // Ensure that badge count == 1 XCTAssertEqual(UIApplication.shared.applicationIconBadgeNumber, 1) }