From 55ef2431fed1a4d2643bc8813fecbfe05da46735 Mon Sep 17 00:00:00 2001 From: Megha Pithadiya Date: Wed, 20 Nov 2024 18:51:21 +0530 Subject: [PATCH 01/16] - Enhance push notification state tracking in SDKs - When the app is brought to the foreground after being in the background and update the notification permission. # Conflicts: # sample-apps/swift-sample-app/swift-sample-app/AppDelegate.swift # sample-apps/swift-sample-app/swift-sample-app/CoffeeListTableViewController.swift # swift-sdk/Core/Constants.swift # swift-sdk/Internal/IterableUserDefaults.swift # swift-sdk/Internal/Utilities/LocalStorage.swift --- swift-sdk/Internal/InternalIterableAPI.swift | 25 +++++++++++++++++++ .../Utilities/LocalStorageProtocol.swift | 2 ++ 2 files changed, 27 insertions(+) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index c80e9d3cb..e47d44560 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -206,6 +206,9 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onFailure?(reason, data) } ) + notificationStateProvider.isNotificationsEnabled { isEnabled in + self.localStorage.isNotificationsEnabled = isEnabled + } } @discardableResult @@ -500,6 +503,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { private var _userId: String? private var _successCallback: OnSuccessHandler? = nil private var _failureCallback: OnFailureHandler? = nil + + private let notificationCenter: NotificationCenterProtocol /// the hex representation of this device token @@ -666,6 +671,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { localStorage = dependencyContainer.localStorage inAppDisplayer = dependencyContainer.inAppDisplayer urlOpener = dependencyContainer.urlOpener + notificationCenter = dependencyContainer.notificationCenter deepLinkManager = DeepLinkManager(redirectNetworkSessionProvider: dependencyContainer) } @@ -698,10 +704,29 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { requestHandler.start() checkRemoteConfiguration() + + addForegroundObservers() return inAppManager.start() } + private func addForegroundObservers() { + NotificationCenter.default.addObserver(self, + selector: #selector(onAppDidBecomeActiveNotification(notification:)), + name: UIApplication.didBecomeActiveNotification, + object: nil) + } + + @objc private func onAppDidBecomeActiveNotification(notification: Notification) { + self.notificationStateProvider.isNotificationsEnabled { isEnabled in + if self.localStorage.isNotificationsEnabled != isEnabled { + if self.config.autoPushRegistration { + self.notificationStateProvider.registerForRemoteNotifications() + } + } + } + } + private func handle(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { guard let launchOptions = launchOptions else { return diff --git a/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift b/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift index 254ce3291..470c83aab 100644 --- a/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift +++ b/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift @@ -19,6 +19,8 @@ protocol LocalStorageProtocol { var offlineMode: Bool { get set } + var isNotificationsEnabled: Bool { get set } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? func save(attributionInfo: IterableAttributionInfo?, withExpiration expiration: Date?) From 8a040501381f87f657da437bf63a7a339cf53434 Mon Sep 17 00:00:00 2001 From: Megha Pithadiya Date: Tue, 17 Dec 2024 19:28:01 +0530 Subject: [PATCH 02/16] Add support of update notifications status with firebase notification integration # Conflicts: # sample-apps/swift-sample-app/swift-sample-app.xcodeproj/project.pbxproj # sample-apps/swift-sample-app/swift-sample-app/AppDelegate.swift # swift-sdk/Internal/InternalIterableAPI.swift --- swift-sdk/Internal/InternalIterableAPI.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index e47d44560..75c7ed710 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -509,6 +509,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { /// the hex representation of this device token private var hexToken: String? + private var isFromFCM: Bool? private var launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -721,7 +722,11 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { self.notificationStateProvider.isNotificationsEnabled { isEnabled in if self.localStorage.isNotificationsEnabled != isEnabled { if self.config.autoPushRegistration { - self.notificationStateProvider.registerForRemoteNotifications() + if let token = self.hexToken, let isFromFCM = self.isFromFCM, isFromFCM { + IterableAPI.registerFCM(token: token) + } else { + self.notificationStateProvider.registerForRemoteNotifications() + } } } } From 82433ce408a11ac9d15a13eb196ef6ff83fa3f29 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Mon, 6 Jan 2025 12:51:51 +0000 Subject: [PATCH 03/16] =?UTF-8?q?=F0=9F=94=A7=20Fixed=20compiler=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk/Core/Constants.swift | 1 + swift-sdk/Internal/IterableUserDefaults.swift | 11 ++++++++++- swift-sdk/Internal/Utilities/LocalStorage.swift | 8 ++++++++ tests/common/MockLocalStorage.swift | 2 ++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/swift-sdk/Core/Constants.swift b/swift-sdk/Core/Constants.swift index 27cce798b..18f4bd539 100644 --- a/swift-sdk/Core/Constants.swift +++ b/swift-sdk/Core/Constants.swift @@ -57,6 +57,7 @@ enum Const { static let deviceId = "itbl_device_id" static let sdkVersion = "itbl_sdk_version" static let offlineMode = "itbl_offline_mode" + static let isNotificationsEnabled = "itbl_is_Notifications_Enabled" static let attributionInfoExpiration = 24 } diff --git a/swift-sdk/Internal/IterableUserDefaults.swift b/swift-sdk/Internal/IterableUserDefaults.swift index 5b5fdaade..4f5391125 100644 --- a/swift-sdk/Internal/IterableUserDefaults.swift +++ b/swift-sdk/Internal/IterableUserDefaults.swift @@ -64,12 +64,20 @@ class IterableUserDefaults { var offlineMode: Bool { get { - return bool(withKey: .offlineMode) + bool(withKey: .offlineMode) } set { save(bool: newValue, withKey: .offlineMode) } } + var isNotificationsEnabled: Bool { + get { + bool(withKey: .isNotificationsEnabled) + } set { + save(bool: newValue, withKey: .isNotificationsEnabled) + } + } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { (try? codable(withKey: .attributionInfo, currentDate: currentDate)) ?? nil } @@ -196,6 +204,7 @@ class IterableUserDefaults { static let deviceId = UserDefaultsKey(value: Const.UserDefault.deviceId) static let sdkVersion = UserDefaultsKey(value: Const.UserDefault.sdkVersion) static let offlineMode = UserDefaultsKey(value: Const.UserDefault.offlineMode) + static let isNotificationsEnabled = UserDefaultsKey(value: Const.UserDefault.isNotificationsEnabled) } private struct Envelope: Codable { diff --git a/swift-sdk/Internal/Utilities/LocalStorage.swift b/swift-sdk/Internal/Utilities/LocalStorage.swift index 9e4a6fcc9..546aea4a8 100644 --- a/swift-sdk/Internal/Utilities/LocalStorage.swift +++ b/swift-sdk/Internal/Utilities/LocalStorage.swift @@ -67,6 +67,14 @@ struct LocalStorage: LocalStorageProtocol { } } + var isNotificationsEnabled: Bool { + get { + iterableUserDefaults.isNotificationsEnabled + } set { + iterableUserDefaults.isNotificationsEnabled = newValue + } + } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { iterableUserDefaults.getAttributionInfo(currentDate: currentDate) } diff --git a/tests/common/MockLocalStorage.swift b/tests/common/MockLocalStorage.swift index ab148e719..54e3d8886 100644 --- a/tests/common/MockLocalStorage.swift +++ b/tests/common/MockLocalStorage.swift @@ -21,6 +21,8 @@ class MockLocalStorage: LocalStorageProtocol { var offlineMode: Bool = false + var isNotificationsEnabled: Bool = false + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { guard !MockLocalStorage.isExpired(expiration: attributionInfoExpiration, currentDate: currentDate) else { return nil From b8571833cb7700a0f2b83890093b18880a692904 Mon Sep 17 00:00:00 2001 From: Evan Greer Date: Tue, 7 Jan 2025 15:09:06 -0700 Subject: [PATCH 04/16] removes reference to FCM --- swift-sdk/Internal/InternalIterableAPI.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index 75c7ed710..e47d44560 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -509,7 +509,6 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { /// the hex representation of this device token private var hexToken: String? - private var isFromFCM: Bool? private var launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -722,11 +721,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { self.notificationStateProvider.isNotificationsEnabled { isEnabled in if self.localStorage.isNotificationsEnabled != isEnabled { if self.config.autoPushRegistration { - if let token = self.hexToken, let isFromFCM = self.isFromFCM, isFromFCM { - IterableAPI.registerFCM(token: token) - } else { - self.notificationStateProvider.registerForRemoteNotifications() - } + self.notificationStateProvider.registerForRemoteNotifications() } } } From 51a699db99423b9654c4429fbaa4163090aef4ad Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 9 Jan 2025 16:25:26 +0000 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=94=A7Added=20unregister=20device?= =?UTF-8?q?=20in=20case=20of=20notifications=20being=20turned=20off.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk/Core/Constants.swift | 2 +- swift-sdk/Internal/InternalIterableAPI.swift | 31 +++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/swift-sdk/Core/Constants.swift b/swift-sdk/Core/Constants.swift index 18f4bd539..ae09f269b 100644 --- a/swift-sdk/Core/Constants.swift +++ b/swift-sdk/Core/Constants.swift @@ -57,7 +57,7 @@ enum Const { static let deviceId = "itbl_device_id" static let sdkVersion = "itbl_sdk_version" static let offlineMode = "itbl_offline_mode" - static let isNotificationsEnabled = "itbl_is_Notifications_Enabled" + static let isNotificationsEnabled = "itbl_isNotificationsEnabled" static let attributionInfoExpiration = 24 } diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index e47d44560..3c5e20ff8 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -711,19 +711,33 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { } private func addForegroundObservers() { - NotificationCenter.default.addObserver(self, - selector: #selector(onAppDidBecomeActiveNotification(notification:)), - name: UIApplication.didBecomeActiveNotification, - object: nil) + notificationCenter.addObserver(self, + selector: #selector(onAppDidBecomeActiveNotification(notification:)), + name: UIApplication.didBecomeActiveNotification, + object: nil) } @objc private func onAppDidBecomeActiveNotification(notification: Notification) { - self.notificationStateProvider.isNotificationsEnabled { isEnabled in - if self.localStorage.isNotificationsEnabled != isEnabled { - if self.config.autoPushRegistration { + // Always update the stored notification state + notificationStateProvider.isNotificationsEnabled { [weak self] isEnabled in + guard let self else { return } + + let previousState = self.localStorage.isNotificationsEnabled + + // Update stored state + self.localStorage.isNotificationsEnabled = isEnabled + + // Only attempt registration/deregistration if state changed and auto-registration is enabled + if previousState != isEnabled && self.config.autoPushRegistration { + if isEnabled { self.notificationStateProvider.registerForRemoteNotifications() - } + } else { + self.disableDeviceForCurrentUser() + } } + + // Check remote configuration after push registration check + self.checkRemoteConfiguration() } } @@ -797,6 +811,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { deinit { ITBInfo() + notificationCenter.removeObserver(self) requestHandler.stop() } } From b92af1dde39e8ee991cec6202d218b1c62403dcd Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 9 Jan 2025 17:05:37 +0000 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=A7=AAAdded=20Unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk.xcodeproj/project.pbxproj | 4 ++ tests/unit-tests/Mocks.swift | 18 ++++---- .../NotificationObserverTests.swift | 44 +++++++++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 tests/unit-tests/NotificationObserverTests.swift diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index 0d2b838f5..d3e1062fb 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 00B6FACE210E88ED007535CF /* prod-1.mobileprovision in Resources */ = {isa = PBXBuildFile; fileRef = 00B6FACD210E874D007535CF /* prod-1.mobileprovision */; }; 00B6FAD1210E8D90007535CF /* dev-1.mobileprovision in Resources */ = {isa = PBXBuildFile; fileRef = 00B6FAD0210E8D90007535CF /* dev-1.mobileprovision */; }; 00CB31B621096129004ACDEC /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00CB31B4210960C4004ACDEC /* TestUtils.swift */; }; + 092D01942D3038F600E3066A /* NotificationObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 092D01932D3038F600E3066A /* NotificationObserverTests.swift */; }; 1CBFFE1A2A97AEEF00ED57EE /* EmbeddedManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */; }; 1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */; }; 1CBFFE1C2A97AEEF00ED57EE /* EmbeddedSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */; }; @@ -543,6 +544,7 @@ 00B6FACD210E874D007535CF /* prod-1.mobileprovision */ = {isa = PBXFileReference; lastKnownFileType = file; path = "prod-1.mobileprovision"; sourceTree = ""; }; 00B6FAD0210E8D90007535CF /* dev-1.mobileprovision */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dev-1.mobileprovision"; sourceTree = ""; }; 00CB31B4210960C4004ACDEC /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; + 092D01932D3038F600E3066A /* NotificationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationObserverTests.swift; sourceTree = ""; }; 1CBFFE162A97AEEE00ED57EE /* EmbeddedManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedManagerTests.swift; sourceTree = ""; }; 1CBFFE172A97AEEE00ED57EE /* EmbeddedMessagingProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedMessagingProcessorTests.swift; sourceTree = ""; }; 1CBFFE182A97AEEE00ED57EE /* EmbeddedSessionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedSessionManagerTests.swift; sourceTree = ""; }; @@ -936,6 +938,7 @@ 552A0AA9280E249C00A80963 /* notification-tests */ = { isa = PBXGroup; children = ( + 092D01932D3038F600E3066A /* NotificationObserverTests.swift */, 55B37FC32297135F0042F13A /* NotificationMetadataTests.swift */, AC2C667F20D31B1F00D46CC9 /* NotificationResponseTests.swift */, ); @@ -2186,6 +2189,7 @@ 5588DFD128C0465E000697D7 /* MockAPNSTypeChecker.swift in Sources */, 00B6FACC210E8484007535CF /* APNSTypeCheckerTests.swift in Sources */, AC8F35A2239806B500302994 /* InboxViewControllerViewModelTests.swift in Sources */, + 092D01942D3038F600E3066A /* NotificationObserverTests.swift in Sources */, AC995F9A2166EEB50099A184 /* CommonMocks.swift in Sources */, 5588DFE128C046B7000697D7 /* MockLocalStorage.swift in Sources */, 1CBFFE1B2A97AEEF00ED57EE /* EmbeddedMessagingProcessorTests.swift in Sources */, diff --git a/tests/unit-tests/Mocks.swift b/tests/unit-tests/Mocks.swift index a76afc556..8e254ab59 100644 --- a/tests/unit-tests/Mocks.swift +++ b/tests/unit-tests/Mocks.swift @@ -10,19 +10,19 @@ import XCTest // Note: This is used only by swift tests. So can't put this in Common class MockNotificationStateProvider: NotificationStateProviderProtocol { - func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) { - callback(enabled) - } - - func registerForRemoteNotifications() { - expectation?.fulfill() - } + var enabled: Bool + private let expectation: XCTestExpectation? init(enabled: Bool, expectation: XCTestExpectation? = nil) { self.enabled = enabled self.expectation = expectation } - private let enabled: Bool - private let expectation: XCTestExpectation? + func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) { + callback(enabled) + } + + func registerForRemoteNotifications() { + expectation?.fulfill() + } } diff --git a/tests/unit-tests/NotificationObserverTests.swift b/tests/unit-tests/NotificationObserverTests.swift new file mode 100644 index 000000000..67109e3b7 --- /dev/null +++ b/tests/unit-tests/NotificationObserverTests.swift @@ -0,0 +1,44 @@ +import XCTest +@testable import IterableSDK + +class NotificationObserverTests: XCTestCase { + private var internalAPI: InternalIterableAPI! + private var mockNotificationStateProvider: MockNotificationStateProvider! + private var mockLocalStorage: MockLocalStorage! + private var mockNotificationCenter: MockNotificationCenter! + + override func setUp() { + super.setUp() + + mockNotificationStateProvider = MockNotificationStateProvider(enabled: false) + mockLocalStorage = MockLocalStorage() + mockNotificationCenter = MockNotificationCenter() + + let config = IterableConfig() + internalAPI = InternalIterableAPI.initializeForTesting( + config: config, + notificationStateProvider: mockNotificationStateProvider, + localStorage: mockLocalStorage, + notificationCenter: mockNotificationCenter + ) + } + + func testNotificationStateChangeUpdatesStorage() { + // Arrange + mockLocalStorage.isNotificationsEnabled = false + mockNotificationStateProvider.enabled = true + + // Act + mockNotificationCenter.post(name: UIApplication.didBecomeActiveNotification, object: nil, userInfo: nil) + + // Small delay to allow async operation to complete + let expectation = XCTestExpectation(description: "Wait for state update") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + expectation.fulfill() + } + wait(for: [expectation], timeout: 1.0) + + // Assert + XCTAssertTrue(mockLocalStorage.isNotificationsEnabled) + } +} From e7c84d3652e48f761ded0dc68f1b435b0ec6a786 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Fri, 10 Jan 2025 08:44:46 +0000 Subject: [PATCH 07/16] =?UTF-8?q?=F0=9F=94=A7=20Track=20first=20foreground?= =?UTF-8?q?=20accordingly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk/Internal/InternalIterableAPI.swift | 29 ++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index 3c5e20ff8..af6cc90b3 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -484,6 +484,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { private var config: IterableConfig private var apiEndPoint: String + private var hasHandledFirstForeground = false /// Following are needed for handling pending notification and deep link. static var pendingNotificationResponse: NotificationResponseProtocol? @@ -718,26 +719,32 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { } @objc private func onAppDidBecomeActiveNotification(notification: Notification) { - // Always update the stored notification state - notificationStateProvider.isNotificationsEnabled { [weak self] isEnabled in + // Track first foreground handling + let isFirstForeground = !hasHandledFirstForeground + hasHandledFirstForeground = true + + // Always check notification state + notificationStateProvider.isNotificationsEnabled { [weak self] systemEnabled in guard let self else { return } - let previousState = self.localStorage.isNotificationsEnabled - - // Update stored state - self.localStorage.isNotificationsEnabled = isEnabled + let storedEnabled = self.localStorage.isNotificationsEnabled - // Only attempt registration/deregistration if state changed and auto-registration is enabled - if previousState != isEnabled && self.config.autoPushRegistration { - if isEnabled { + // Handle push registration/deregistration if needed + if self.config.autoPushRegistration && storedEnabled != systemEnabled { + if systemEnabled { self.notificationStateProvider.registerForRemoteNotifications() } else { self.disableDeviceForCurrentUser() } } - // Check remote configuration after push registration check - self.checkRemoteConfiguration() + // Only fetch remote configuration on first foreground + if isFirstForeground { + self.checkRemoteConfiguration() + } + + // Always update stored state + self.localStorage.isNotificationsEnabled = systemEnabled } } From c1a7038a7e7157037ad10246875ca368356d2d68 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Fri, 10 Jan 2025 13:36:09 +0100 Subject: [PATCH 08/16] [MOB-9445] Fixes to logic --- swift-sdk/Core/Constants.swift | 3 +- swift-sdk/Internal/InternalIterableAPI.swift | 31 ++++++------------- swift-sdk/Internal/IterableUserDefaults.swift | 9 ++++++ .../Internal/Utilities/LocalStorage.swift | 8 +++++ .../Utilities/LocalStorageProtocol.swift | 2 ++ tests/common/MockLocalStorage.swift | 4 ++- 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/swift-sdk/Core/Constants.swift b/swift-sdk/Core/Constants.swift index 3702c5a2b..9fd068404 100644 --- a/swift-sdk/Core/Constants.swift +++ b/swift-sdk/Core/Constants.swift @@ -58,7 +58,8 @@ enum Const { static let sdkVersion = "itbl_sdk_version" static let offlineMode = "itbl_offline_mode" static let isNotificationsEnabled = "itbl_isNotificationsEnabled" - + static let hasStoredNotificationSetting = "itbl_hasStoredNotificationSetting" + static let attributionInfoExpiration = 24 } diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index af6cc90b3..5f9de88d8 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -206,9 +206,6 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onFailure?(reason, data) } ) - notificationStateProvider.isNotificationsEnabled { isEnabled in - self.localStorage.isNotificationsEnabled = isEnabled - } } @discardableResult @@ -484,7 +481,6 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { private var config: IterableConfig private var apiEndPoint: String - private var hasHandledFirstForeground = false /// Following are needed for handling pending notification and deep link. static var pendingNotificationResponse: NotificationResponseProtocol? @@ -719,32 +715,23 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { } @objc private func onAppDidBecomeActiveNotification(notification: Notification) { - // Track first foreground handling - let isFirstForeground = !hasHandledFirstForeground - hasHandledFirstForeground = true - + let storedEnabled = self.localStorage.isNotificationsEnabled + let hasStoredPermission = self.localStorage.hasStoredNotificationSetting + // Always check notification state notificationStateProvider.isNotificationsEnabled { [weak self] systemEnabled in - guard let self else { return } + guard let self = self else { return } - let storedEnabled = self.localStorage.isNotificationsEnabled - - // Handle push registration/deregistration if needed - if self.config.autoPushRegistration && storedEnabled != systemEnabled { - if systemEnabled { - self.notificationStateProvider.registerForRemoteNotifications() - } else { + // Only handle permission changes if we've previously stored a permission state + if hasStoredPermission && (storedEnabled != systemEnabled) { + if !systemEnabled { self.disableDeviceForCurrentUser() } } - // Only fetch remote configuration on first foreground - if isFirstForeground { - self.checkRemoteConfiguration() - } - - // Always update stored state + // Always store the current state self.localStorage.isNotificationsEnabled = systemEnabled + self.localStorage.hasStoredNotificationSetting = true } } diff --git a/swift-sdk/Internal/IterableUserDefaults.swift b/swift-sdk/Internal/IterableUserDefaults.swift index 4f5391125..5c11ec791 100644 --- a/swift-sdk/Internal/IterableUserDefaults.swift +++ b/swift-sdk/Internal/IterableUserDefaults.swift @@ -78,6 +78,14 @@ class IterableUserDefaults { } } + var hasStoredNotificationSetting: Bool { + get { + bool(withKey: .hasStoredNotificationSetting) + } set { + save(bool: newValue, withKey: .hasStoredNotificationSetting) + } + } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { (try? codable(withKey: .attributionInfo, currentDate: currentDate)) ?? nil } @@ -205,6 +213,7 @@ class IterableUserDefaults { static let sdkVersion = UserDefaultsKey(value: Const.UserDefault.sdkVersion) static let offlineMode = UserDefaultsKey(value: Const.UserDefault.offlineMode) static let isNotificationsEnabled = UserDefaultsKey(value: Const.UserDefault.isNotificationsEnabled) + static let hasStoredNotificationSetting = UserDefaultsKey(value: Const.UserDefault.hasStoredNotificationSetting) } private struct Envelope: Codable { diff --git a/swift-sdk/Internal/Utilities/LocalStorage.swift b/swift-sdk/Internal/Utilities/LocalStorage.swift index 546aea4a8..a6e049ec6 100644 --- a/swift-sdk/Internal/Utilities/LocalStorage.swift +++ b/swift-sdk/Internal/Utilities/LocalStorage.swift @@ -75,6 +75,14 @@ struct LocalStorage: LocalStorageProtocol { } } + var hasStoredNotificationSetting: Bool { + get { + iterableUserDefaults.hasStoredNotificationSetting + } set { + iterableUserDefaults.hasStoredNotificationSetting = newValue + } + } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { iterableUserDefaults.getAttributionInfo(currentDate: currentDate) } diff --git a/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift b/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift index 470c83aab..420d076db 100644 --- a/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift +++ b/swift-sdk/Internal/Utilities/LocalStorageProtocol.swift @@ -21,6 +21,8 @@ protocol LocalStorageProtocol { var isNotificationsEnabled: Bool { get set } + var hasStoredNotificationSetting: Bool { get set } + func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? func save(attributionInfo: IterableAttributionInfo?, withExpiration expiration: Date?) diff --git a/tests/common/MockLocalStorage.swift b/tests/common/MockLocalStorage.swift index 54e3d8886..56aaed4f0 100644 --- a/tests/common/MockLocalStorage.swift +++ b/tests/common/MockLocalStorage.swift @@ -21,7 +21,9 @@ class MockLocalStorage: LocalStorageProtocol { var offlineMode: Bool = false - var isNotificationsEnabled: Bool = false + var isNotificationsEnabled: Bool = false + + var hasStoredNotificationSetting: Bool = false func getAttributionInfo(currentDate: Date) -> IterableAttributionInfo? { guard !MockLocalStorage.isExpired(expiration: attributionInfoExpiration, currentDate: currentDate) else { From caaeb0f93dec785d5371e2c768d835424ccf6a1f Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Fri, 10 Jan 2025 13:37:23 +0100 Subject: [PATCH 09/16] [MOB-9445] Fixes to logic --- swift-sdk/Internal/Utilities/LocalStorage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swift-sdk/Internal/Utilities/LocalStorage.swift b/swift-sdk/Internal/Utilities/LocalStorage.swift index a6e049ec6..570803313 100644 --- a/swift-sdk/Internal/Utilities/LocalStorage.swift +++ b/swift-sdk/Internal/Utilities/LocalStorage.swift @@ -61,7 +61,7 @@ struct LocalStorage: LocalStorageProtocol { var offlineMode: Bool { get { - iterableUserDefaults.offlineMode + return iterableUserDefaults.offlineMode } set { iterableUserDefaults.offlineMode = newValue } From 0324cfe6a3d1ec9901b7ecb23fdcc963c93a03c2 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Fri, 10 Jan 2025 13:38:14 +0100 Subject: [PATCH 10/16] [MOB-9445] Fixes to logic --- swift-sdk/Internal/Utilities/LocalStorage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swift-sdk/Internal/Utilities/LocalStorage.swift b/swift-sdk/Internal/Utilities/LocalStorage.swift index 570803313..a6e049ec6 100644 --- a/swift-sdk/Internal/Utilities/LocalStorage.swift +++ b/swift-sdk/Internal/Utilities/LocalStorage.swift @@ -61,7 +61,7 @@ struct LocalStorage: LocalStorageProtocol { var offlineMode: Bool { get { - return iterableUserDefaults.offlineMode + iterableUserDefaults.offlineMode } set { iterableUserDefaults.offlineMode = newValue } From 76b38e8e8ac206109b6f80195540e74316fa1138 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Fri, 10 Jan 2025 17:53:52 +0100 Subject: [PATCH 11/16] [MOB-9445] Logic fix --- swift-sdk/Internal/InternalIterableAPI.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index 5f9de88d8..faa60a90d 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -726,6 +726,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { if hasStoredPermission && (storedEnabled != systemEnabled) { if !systemEnabled { self.disableDeviceForCurrentUser() + } else { + notificationStateProvider.registerForRemoteNotifications() } } From 7a6ec97a269829bb231a162b1487f4b12d0bb440 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Tue, 14 Jan 2025 14:30:27 +0000 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=94=A7=20Added=20convenience=20meth?= =?UTF-8?q?od?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk/Internal/InternalIterableAPI.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index faa60a90d..93b5f3075 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -176,7 +176,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { // MARK: - API Request Calls - func register(token: Data, + func register(token: String, onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) { guard let appName = pushIntegrationName else { @@ -187,8 +187,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { return } - hexToken = token.hexString() - let registerTokenInfo = RegisterTokenInfo(hexToken: token.hexString(), + hexToken = token + let registerTokenInfo = RegisterTokenInfo(hexToken: token, appName: appName, pushServicePlatform: config.pushPlatform, apnsType: dependencyContainer.apnsTypeChecker.apnsType, @@ -208,6 +208,12 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { ) } + func register(token: Data, + onSuccess: OnSuccessHandler? = nil, + onFailure: OnFailureHandler? = nil) { + register(token: token.hexString(), onSuccess: onSuccess, onFailure: onFailure) + } + @discardableResult func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = nil, onFailure: OnFailureHandler? = nil) -> Pending { From 29a4c9dbbedca0ff53068615a3437d7551674d10 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Tue, 14 Jan 2025 14:33:01 +0000 Subject: [PATCH 13/16] =?UTF-8?q?=F0=9F=94=A7=20Updated=20logic=20to=20cor?= =?UTF-8?q?rectly=20handle=20notifications=20updates=20on=20the=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk/Internal/InternalIterableAPI.swift | 33 +++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index 93b5f3075..154812441 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -222,12 +222,15 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { onFailure?(errorMessage, nil) return SendRequestError.createErroredFuture(reason: errorMessage) } + guard userId != nil || email != nil else { let errorMessage = "either userId or email must be present" onFailure?(errorMessage, nil) return SendRequestError.createErroredFuture(reason: errorMessage) } + register(token: hexToken) + return requestHandler.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure) } @@ -721,25 +724,27 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { } @objc private func onAppDidBecomeActiveNotification(notification: Notification) { - let storedEnabled = self.localStorage.isNotificationsEnabled - let hasStoredPermission = self.localStorage.hasStoredNotificationSetting - - // Always check notification state + guard config.autoPushRegistration else { return } + notificationStateProvider.isNotificationsEnabled { [weak self] systemEnabled in guard let self = self else { return } - // Only handle permission changes if we've previously stored a permission state - if hasStoredPermission && (storedEnabled != systemEnabled) { - if !systemEnabled { - self.disableDeviceForCurrentUser() - } else { - notificationStateProvider.registerForRemoteNotifications() + let storedEnabled = self.localStorage.isNotificationsEnabled + let hasStoredPermission = self.localStorage.hasStoredNotificationSetting + + if self.isEitherUserIdOrEmailSet() { + if hasStoredPermission && (storedEnabled != systemEnabled) { + if !systemEnabled { + self.disableDeviceForCurrentUser() + } else { + self.notificationStateProvider.registerForRemoteNotifications() + } } + + // Always store the current state + self.localStorage.isNotificationsEnabled = systemEnabled + self.localStorage.hasStoredNotificationSetting = true } - - // Always store the current state - self.localStorage.isNotificationsEnabled = systemEnabled - self.localStorage.hasStoredNotificationSetting = true } } From 2c6186e9845a5d35df61a8d0f4438dbad56c2944 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Tue, 14 Jan 2025 15:17:28 +0000 Subject: [PATCH 14/16] =?UTF-8?q?=E2=9C=8D=EF=B8=8F=20Added=20comment=20fo?= =?UTF-8?q?r=20clarity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk/Internal/InternalIterableAPI.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index 154812441..fc07929cd 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -229,6 +229,9 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { return SendRequestError.createErroredFuture(reason: errorMessage) } + // We need to call register token here so that we can trigger the device registration + // with the updated notification settings + register(token: hexToken) return requestHandler.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure) From 0d5d725e703bea55e5aa2f780aff6f179d9f4b9e Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Tue, 14 Jan 2025 15:51:40 +0000 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=A7=AA=20Updated=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit-tests/AutoRegistrationTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit-tests/AutoRegistrationTests.swift b/tests/unit-tests/AutoRegistrationTests.swift index 47d0e0ab2..b2441d362 100644 --- a/tests/unit-tests/AutoRegistrationTests.swift +++ b/tests/unit-tests/AutoRegistrationTests.swift @@ -20,6 +20,7 @@ class AutoRegistrationTests: XCTestCase { func testCallDisableAndEnable() { let expectation1 = expectation(description: "call register device API") + expectation1.expectedFulfillmentCount = 2 let expectation2 = expectation(description: "call registerForRemoteNotifications twice") expectation2.expectedFulfillmentCount = 2 let expectation3 = expectation(description: "call disable on user1@example.com") From d6430a43bd5d5c8bd6a794e053843218c504aba4 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Tue, 14 Jan 2025 16:02:23 +0000 Subject: [PATCH 16/16] =?UTF-8?q?=F0=9F=A7=AA=20Updated=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit-tests/NotificationObserverTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit-tests/NotificationObserverTests.swift b/tests/unit-tests/NotificationObserverTests.swift index 67109e3b7..3e410b17b 100644 --- a/tests/unit-tests/NotificationObserverTests.swift +++ b/tests/unit-tests/NotificationObserverTests.swift @@ -25,6 +25,8 @@ class NotificationObserverTests: XCTestCase { func testNotificationStateChangeUpdatesStorage() { // Arrange + internalAPI.email = "johnappleseed@iterable.com" + mockLocalStorage.isNotificationsEnabled = false mockNotificationStateProvider.enabled = true