diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be5eeb6d..2c7849733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,47 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 6.4.16 +### Added +- `sendRequestWithRetries` function added as part of the `NetworkHelperclass` +- For an API request that results in a 500-level error response, the SDK now executes up to five retries. Before each of the final three attempts, there is a two-second delay. + +### Changed +- updates `sendRequest` in `RequestProcessorUtil` to retry the API request that resulted in a 401 response upon receipt of a new JWT +- updates `NetworkHelper` class logic to use `sendRequestWithRetries`method which wraps the original `networkSession.makeRequest` +- When an API request fails with a 401 because of an invalid JWT token, the SDK now immediately requests a new JWT token for the signed-in user. + +## 6.4.15 +### Added +- This release allows you to use projects hosted on Iterable's EU data center. If your project is hosted on Iterable's [European data center (EUDC)](https://support.iterable.com/hc/articles/17572750887444), configure the SDK to use Iterable's EU-based API endpoints: + +_Swift_ + +```swift +let config = IterableConfig() +config.dataRegion = IterableDataRegion.EU +IterableAPI.initialize(apiKey: "", launchOptions: launchOptions, config: config) +``` + +_Objective-C_ + +```objectivec +IterableConfig *config = [[IterableConfig alloc] init]; +config.dataRegion = IterableDataRegion.EU; +[IterableAPI initializeWithApiKey:@"" launchOptions:launchOptions config:config]; +``` + +### Fixed +- Offline Mode is now off by default. Offline mode components will only load when the `offlineMode` configuration for RequestHandler is set to true. + +### Changed +- Offline mode configuration now persists throughout the current app session. Changes will take effect from the next app session. + +## 6.4.14 +### Added +- Success and Failure handlers can now be passed to following functions: +`InAppManager.remove`, `InAppManager.setRead`, `IterableAPI.setEmail` and `IterableAPI.setUserId` + ## 6.4.13 ### Added - `ITBNotificationServiceExtension` has a new optional delegate in the scenario of wanting to receive and pass along push information (e.g. Firebase) diff --git a/Iterable-iOS-AppExtensions.podspec b/Iterable-iOS-AppExtensions.podspec index c8f405671..2fe946d62 100644 --- a/Iterable-iOS-AppExtensions.podspec +++ b/Iterable-iOS-AppExtensions.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "Iterable-iOS-AppExtensions" s.module_name = "IterableAppExtensions" - s.version = "6.4.13" + s.version = "6.4.16" s.summary = "App Extensions for Iterable SDK" s.description = <<-DESC diff --git a/Iterable-iOS-SDK.podspec b/Iterable-iOS-SDK.podspec index c3ca66e47..adffcf24e 100644 --- a/Iterable-iOS-SDK.podspec +++ b/Iterable-iOS-SDK.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "Iterable-iOS-SDK" s.module_name = "IterableSDK" - s.version = "6.4.13" + s.version = "6.4.16" s.summary = "Iterable's official SDK for iOS" s.description = <<-DESC diff --git a/swift-sdk.xcodeproj/project.pbxproj b/swift-sdk.xcodeproj/project.pbxproj index cc6b211bd..8d0375b94 100644 --- a/swift-sdk.xcodeproj/project.pbxproj +++ b/swift-sdk.xcodeproj/project.pbxproj @@ -185,6 +185,13 @@ 5B5AA717284F1A6D0093FED4 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5AA710284F1A6D0093FED4 /* MockNetworkSession.swift */; }; 5B6C3C1127CE871F00B9A753 /* NavInboxSessionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6C3C1027CE871F00B9A753 /* NavInboxSessionUITests.swift */; }; 5B88BC482805D09D004016E5 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B88BC472805D09D004016E5 /* NetworkSession.swift */; }; + 9FF05EAC2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; }; + 9FF05EAD2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; }; + 9FF05EAE2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; }; + 9FF05EAF2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; }; + 9FF05EB02AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; }; + 9FF05EB12AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; }; + 9FF05EB22AFEA5FA005311F7 /* MockAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */; }; AC02480822791E2100495FB9 /* IterableInboxNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC02480722791E2100495FB9 /* IterableInboxNavigationViewController.swift */; }; AC02CAA6234E50B5006617E0 /* RegistrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC02CAA5234E50B5006617E0 /* RegistrationTests.swift */; }; AC03094B21E532470003A288 /* InAppPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC03094A21E532470003A288 /* InAppPersistence.swift */; }; @@ -594,6 +601,7 @@ 5B6C3C1027CE871F00B9A753 /* NavInboxSessionUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavInboxSessionUITests.swift; sourceTree = ""; }; 5B88BC472805D09D004016E5 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = ""; }; 5BFC7CED27FC9AF300E77479 /* inbox-ui-tests-app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "inbox-ui-tests-app.entitlements"; sourceTree = ""; }; + 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthManager.swift; sourceTree = ""; }; AC02480722791E2100495FB9 /* IterableInboxNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableInboxNavigationViewController.swift; sourceTree = ""; }; AC02CAA5234E50B5006617E0 /* RegistrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationTests.swift; sourceTree = ""; }; AC03094A21E532470003A288 /* InAppPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPersistence.swift; sourceTree = ""; }; @@ -1415,6 +1423,7 @@ 5588DF7D28C04494000697D7 /* MockUrlDelegate.swift */, 5588DF8D28C044DE000697D7 /* MockUrlOpener.swift */, 5588DFD528C04683000697D7 /* MockWebView.swift */, + 9FF05EAB2AFEA5FA005311F7 /* MockAuthManager.swift */, ); path = common; sourceTree = ""; @@ -2153,6 +2162,7 @@ ACA2A91A24AB266F001DFD17 /* Mocks.swift in Sources */, 5588DFAB28C045AE000697D7 /* MockInAppFetcher.swift in Sources */, AC29D05C24B5A7E000A9E019 /* CI.swift in Sources */, + 9FF05EB12AFEA5FA005311F7 /* MockAuthManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2166,6 +2176,7 @@ AC2C668720D3435700D46CC9 /* ActionRunnerTests.swift in Sources */, 00CB31B621096129004ACDEC /* TestUtils.swift in Sources */, AC89661E2124FBCE0051A6CD /* AutoRegistrationTests.swift in Sources */, + 9FF05EAF2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */, ACA8D1A921965B7D001B1332 /* InAppTests.swift in Sources */, 5588DFB928C045E3000697D7 /* MockInAppDelegate.swift in Sources */, 5588DFD128C0465E000697D7 /* MockAPNSTypeChecker.swift in Sources */, @@ -2266,6 +2277,7 @@ buildActionMask = 2147483647; files = ( 5588DFB828C045E3000697D7 /* MockInAppDelegate.swift in Sources */, + 9FF05EAE2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */, 5588DF7028C0442D000697D7 /* MockDateProvider.swift in Sources */, 5588DFF028C046FF000697D7 /* MockMessageViewControllerEventTracker.swift in Sources */, ACB37AB124026C1E0093A8EA /* SampleInboxViewDelegateImplementations.swift in Sources */, @@ -2329,6 +2341,7 @@ ACC6A8502323910D003CC4BE /* UITestsHelper.swift in Sources */, 5588DFAA28C045AE000697D7 /* MockInAppFetcher.swift in Sources */, ACFF4287246569D300FDF10D /* CommonExtensions.swift in Sources */, + 9FF05EB02AFEA5FA005311F7 /* MockAuthManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2355,6 +2368,7 @@ 5588DFCE28C0465E000697D7 /* MockAPNSTypeChecker.swift in Sources */, 5588DFDE28C046B7000697D7 /* MockLocalStorage.swift in Sources */, ACA8D1A52196309C001B1332 /* Common.swift in Sources */, + 9FF05EAC2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */, 5588DFB628C045E3000697D7 /* MockInAppDelegate.swift in Sources */, 5588DF8628C044BE000697D7 /* MockCustomActionDelegate.swift in Sources */, AC995F992166EE490099A184 /* CommonMocks.swift in Sources */, @@ -2393,6 +2407,7 @@ 5588DF7C28C04463000697D7 /* MockNotificationResponse.swift in Sources */, 5588DFD428C0465E000697D7 /* MockAPNSTypeChecker.swift in Sources */, 5588DFE428C046B7000697D7 /* MockLocalStorage.swift in Sources */, + 9FF05EB22AFEA5FA005311F7 /* MockAuthManager.swift in Sources */, ACC362C924D2CA8C002C67BA /* Common.swift in Sources */, ACC362C524D2C190002C67BA /* TaskProcessorTests.swift in Sources */, AC2AED4224EBC60C000EE5F3 /* TaskRunnerTests.swift in Sources */, @@ -2411,6 +2426,7 @@ 5588DFE728C046D7000697D7 /* MockInboxState.swift in Sources */, ACFF42A424656CCE00FDF10D /* ViewController.swift in Sources */, 5588DF7F28C04494000697D7 /* MockUrlDelegate.swift in Sources */, + 9FF05EAD2AFEA5FA005311F7 /* MockAuthManager.swift in Sources */, 5588DF8728C044BE000697D7 /* MockCustomActionDelegate.swift in Sources */, 5588DFD728C04683000697D7 /* MockWebView.swift in Sources */, 5588DFEF28C046FF000697D7 /* MockMessageViewControllerEventTracker.swift in Sources */, diff --git a/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme b/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme index 5749aca4b..570b83965 100644 --- a/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme +++ b/swift-sdk.xcodeproj/xcshareddata/xcschemes/swift-sdk.xcscheme @@ -210,6 +210,11 @@ BlueprintName = "offline-events-tests" ReferencedContainer = "container:swift-sdk.xcodeproj"> + + + + diff --git a/swift-sdk/Constants.swift b/swift-sdk/Constants.swift index 68a237e04..c8335f590 100644 --- a/swift-sdk/Constants.swift +++ b/swift-sdk/Constants.swift @@ -289,6 +289,11 @@ enum JsonValue { } } +public enum IterableDataRegion { + public static let US = "https://api.iterable.com/api/" + public static let EU = "https://api.eu.iterable.com/api/" +} + public protocol JsonValueRepresentable { var jsonValue: Any { get } } diff --git a/swift-sdk/Internal/DependencyContainerProtocol.swift b/swift-sdk/Internal/DependencyContainerProtocol.swift index fb1da5179..0b88a7012 100644 --- a/swift-sdk/Internal/DependencyContainerProtocol.swift +++ b/swift-sdk/Internal/DependencyContainerProtocol.swift @@ -78,21 +78,33 @@ extension DependencyContainerProtocol { networkSession: networkSession, deviceMetadata: deviceMetadata, dateProvider: dateProvider) - if let persistenceContextProvider = createPersistenceContextProvider() { + lazy var offlineProcessor: OfflineRequestProcessor? = nil + lazy var healthMonitor: HealthMonitor? = nil + guard let persistenceContextProvider = createPersistenceContextProvider() else { + return RequestHandler(onlineProcessor: onlineProcessor, + offlineProcessor: nil, + healthMonitor: nil, + offlineMode: offlineMode) + } + if offlineMode { + let healthMonitorDataProvider = createHealthMonitorDataProvider(persistenceContextProvider: persistenceContextProvider) - let healthMonitor = HealthMonitor(dataProvider: healthMonitorDataProvider, - dateProvider: dateProvider, - networkSession: networkSession) - let offlineProcessor = OfflineRequestProcessor(apiKey: apiKey, - authProvider: authProvider, - authManager: authManager, - endpoint: endpoint, - deviceMetadata: deviceMetadata, - taskScheduler: createTaskScheduler(persistenceContextProvider: persistenceContextProvider, - healthMonitor: healthMonitor), - taskRunner: createTaskRunner(persistenceContextProvider: persistenceContextProvider, - healthMonitor: healthMonitor), - notificationCenter: notificationCenter) + + healthMonitor = HealthMonitor(dataProvider: healthMonitorDataProvider, + dateProvider: dateProvider, + networkSession: networkSession) + offlineProcessor = OfflineRequestProcessor(apiKey: apiKey, + authProvider: authProvider, + authManager: authManager, + endpoint: endpoint, + deviceMetadata: deviceMetadata, + taskScheduler: createTaskScheduler(persistenceContextProvider: persistenceContextProvider, + healthMonitor: healthMonitor!), + taskRunner: createTaskRunner(persistenceContextProvider: persistenceContextProvider, + healthMonitor: healthMonitor!), + notificationCenter: notificationCenter) + + return RequestHandler(onlineProcessor: onlineProcessor, offlineProcessor: offlineProcessor, healthMonitor: healthMonitor, diff --git a/swift-sdk/Internal/EmptyInAppManager.swift b/swift-sdk/Internal/EmptyInAppManager.swift index 7f82d9569..26a584757 100644 --- a/swift-sdk/Internal/EmptyInAppManager.swift +++ b/swift-sdk/Internal/EmptyInAppManager.swift @@ -6,6 +6,7 @@ import Foundation import UIKit class EmptyInAppManager: IterableInternalInAppManagerProtocol { + func start() -> Pending { Fulfill(value: true) } @@ -34,14 +35,24 @@ class EmptyInAppManager: IterableInternalInAppManagerProtocol { func remove(message _: IterableInAppMessage) {} + func remove(message _: IterableInAppMessage, successHandler _: OnSuccessHandler?, failureHandler _: OnFailureHandler?) {} + func remove(message _: IterableInAppMessage, location _: InAppLocation) {} + func remove(message _: IterableInAppMessage, location _: InAppLocation, successHandler _: OnSuccessHandler?, failureHandler _: OnFailureHandler?) {} + func remove(message _: IterableInAppMessage, location _: InAppLocation, source _: InAppDeleteSource) {} + func remove(message _: IterableInAppMessage, location _: InAppLocation, source _: InAppDeleteSource, successHandler _: OnSuccessHandler?, failureHandler _: OnFailureHandler?) {} + func remove(message _: IterableInAppMessage, location _: InAppLocation, source _: InAppDeleteSource, inboxSessionId _: String?) {} + func remove(message _: IterableInAppMessage, location _: InAppLocation, source _: InAppDeleteSource, inboxSessionId _: String?, successHandler _: OnSuccessHandler?, failureHandler _: OnFailureHandler?) {} + func set(read _: Bool, forMessage _: IterableInAppMessage) {} + func set(read _: Bool, forMessage _: IterableInAppMessage, successHandler _: OnSuccessHandler?, failureHandler _: OnFailureHandler?) {} + func getMessage(withId _: String) -> IterableInAppMessage? { nil } diff --git a/swift-sdk/Internal/InAppManager.swift b/swift-sdk/Internal/InAppManager.swift index 130919faf..eff886a5c 100644 --- a/swift-sdk/Internal/InAppManager.swift +++ b/swift-sdk/Internal/InAppManager.swift @@ -19,14 +19,24 @@ protocol IterableInternalInAppManagerProtocol: IterableInAppManagerProtocol, InA /// - parameter inboxSessionId: The ID of the inbox session that the message originates from. func handleClick(clickedUrl url: URL?, forMessage message: IterableInAppMessage, location: InAppLocation, inboxSessionId: String?) + /// - parameter message: The message to remove. /// - parameter location: The location from where this message was shown. `inbox` or `inApp`. /// - parameter source: The source of deletion `inboxSwipe` or `deleteButton`.` /// - parameter inboxSessionId: The ID of the inbox session that the message originates from. func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource, inboxSessionId: String?) + + /// - parameter message: The message to remove. + /// - parameter location: The location from where this message was shown. `inbox` or `inApp`. + /// - parameter source: The source of deletion `inboxSwipe` or `deleteButton`.` + /// - parameter inboxSessionId: The ID of the inbox session that the message originates from. + /// - parameter successHandler: The callback which returns `success. + /// - parameter failureHandler: The callback which returns `failure. + func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource, inboxSessionId: String?, successHandler: OnSuccessHandler?, failureHandler: OnFailureHandler?) } class InAppManager: NSObject, IterableInternalInAppManagerProtocol { + init(requestHandler: RequestHandlerProtocol, deviceMetadata: DeviceMetadata, fetcher: InAppFetcherProtocol, @@ -124,26 +134,44 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol { func remove(message: IterableInAppMessage, location: InAppLocation) { ITBInfo() - removePrivate(message: message, location: location) + remove(message: message, location: location, successHandler: nil, failureHandler: nil) + } + + func remove(message: IterableInAppMessage, location: InAppLocation, successHandler: OnSuccessHandler? = nil, failureHandler: OnFailureHandler? = nil) { + removePrivate(message: message, location: location, successHandler: successHandler, failureHandler: failureHandler) } func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource) { - ITBInfo() + remove(message: message, location: location, source: source, successHandler: nil, failureHandler: nil) + } + + func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource, successHandler: OnSuccessHandler? = nil, failureHandler: OnFailureHandler? = nil) { - removePrivate(message: message, location: location, source: source) + removePrivate(message: message, location: location, source: source, successHandler: successHandler, failureHandler: failureHandler) } - func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource, inboxSessionId: String? = nil) { + func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource, inboxSessionId: String?) { ITBInfo() - removePrivate(message: message, location: location, source: source, inboxSessionId: inboxSessionId) + remove(message: message, location: location, source: source, inboxSessionId: inboxSessionId, successHandler: nil, failureHandler: nil) + } + + func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource, inboxSessionId: String? = nil, successHandler: OnSuccessHandler? = nil, failureHandler: OnFailureHandler? = nil) { + removePrivate(message: message, location: location, source: source, inboxSessionId: inboxSessionId, successHandler: successHandler, failureHandler: failureHandler) } func set(read: Bool, forMessage message: IterableInAppMessage) { + set(read: read, forMessage: message, successHandler: nil, failureHandler: nil) + } + + func set(read: Bool, forMessage message: IterableInAppMessage, successHandler: OnSuccessHandler? = nil, failureHandler: OnFailureHandler? = nil) { updateMessage(message, read: read).onSuccess { [weak self] _ in + successHandler?([:]) self?.callbackQueue.async { [weak self] in self?.notificationCenter.post(name: .iterableInboxChanged, object: self, userInfo: nil) } + }.onError { [weak self] _ in + failureHandler?(self?.description, nil) } } @@ -185,8 +213,12 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol { func remove(message: IterableInAppMessage) { ITBInfo() - - removePrivate(message: message) + + remove(message: message, successHandler: nil, failureHandler: nil) + } + + func remove(message: IterableInAppMessage, successHandler: OnSuccessHandler?, failureHandler: OnFailureHandler?) { + removePrivate(message: message, location: .inApp, source: nil, successHandler: successHandler, failureHandler: failureHandler) } // MARK: - Private/Internal @@ -462,16 +494,17 @@ class InAppManager: NSObject, IterableInternalInAppManagerProtocol { private func removePrivate(message: IterableInAppMessage, location: InAppLocation = .inApp, source: InAppDeleteSource? = nil, - inboxSessionId: String? = nil) { + inboxSessionId: String? = nil, + successHandler: OnSuccessHandler? = nil, + failureHandler: OnFailureHandler? = nil) { ITBInfo() - updateMessage(message, didProcessTrigger: true, consumed: true) requestHandler?.inAppConsume(message: message, location: location, source: source, inboxSessionId: inboxSessionId, - onSuccess: nil, - onFailure: nil) + onSuccess: successHandler, + onFailure: failureHandler) callbackQueue.async { [weak self] in self?.notificationCenter.post(name: .iterableInboxChanged, object: self, userInfo: nil) } diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index d12795323..2c4b80645 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -82,10 +82,18 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { self.dependencyContainer.createAuthManager(config: self.config) }() +<<<<<<< HEAD lazy var embeddedManager: IterableEmbeddedManagerProtocol = { self.dependencyContainer.createEmbeddedManager(config: self.config, apiClient: self.apiClient) }() +======= + var apiEndPointForTest: String { + get { + apiEndPoint + } + } +>>>>>>> master // MARK: - SDK Functions @@ -116,7 +124,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { _payloadData = data } - func setEmail(_ email: String?, authToken: String? = nil) { + func setEmail(_ email: String?, authToken: String? = nil, successHandler: OnSuccessHandler? = nil, failureHandler: OnFailureHandler? = nil) { ITBInfo() if _email == email && email != nil && authToken != nil { @@ -132,13 +140,15 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { _email = email _userId = nil + _successCallback = successHandler + _failureCallback = failureHandler storeIdentifierData() onLogin(authToken) } - func setUserId(_ userId: String?, authToken: String? = nil) { + func setUserId(_ userId: String?, authToken: String? = nil, successHandler: OnSuccessHandler? = nil, failureHandler: OnFailureHandler? = nil) { ITBInfo() if _userId == userId && userId != nil && authToken != nil { @@ -154,6 +164,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { _email = nil _userId = userId + _successCallback = successHandler + _failureCallback = failureHandler storeIdentifierData() @@ -172,6 +184,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { guard let appName = pushIntegrationName else { let errorMessage = "Not registering device token - appName must not be nil" ITBError(errorMessage) + _failureCallback?(errorMessage, nil) onFailure?(errorMessage, nil) return } @@ -186,8 +199,15 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { sdkVersion: localStorage.sdkVersion) requestHandler.register(registerTokenInfo: registerTokenInfo, notificationStateProvider: notificationStateProvider, - onSuccess: onSuccess, - onFailure: onFailure) + onSuccess: { (_ data: [AnyHashable: Any]?) in + self._successCallback?(data) + onSuccess?(data) + }, + onFailure: { (_ reason: String?, _ data: Data?) in + self._failureCallback?(reason, data) + onFailure?(reason, data) + } + ) } @discardableResult @@ -480,6 +500,9 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { private var _email: String? private var _payloadData: [AnyHashable: Any]? private var _userId: String? + private var _successCallback: OnSuccessHandler? = nil + private var _failureCallback: OnFailureHandler? = nil + /// the hex representation of this device token private var hexToken: String? @@ -590,6 +613,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { if config.autoPushRegistration { notificationStateProvider.registerForRemoteNotifications() + } else { + _successCallback?([:]) } _ = inAppManager.scheduleSync() @@ -619,6 +644,11 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { } } + private static func setApiEndpoint(apiEndPointOverride: String?, config: IterableConfig) -> String { + let apiEndPoint = config.dataRegion + return apiEndPointOverride ?? apiEndPoint + } + init(apiKey: String, launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil, config: IterableConfig = IterableConfig(), @@ -629,7 +659,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { self.apiKey = apiKey self.launchOptions = launchOptions self.config = config - apiEndPoint = apiEndPointOverride ?? Endpoint.api + apiEndPoint = InternalIterableAPI.setApiEndpoint(apiEndPointOverride: apiEndPointOverride, config: config) self.dependencyContainer = dependencyContainer dateProvider = dependencyContainer.dateProvider networkSession = dependencyContainer.networkSession diff --git a/swift-sdk/Internal/IterableCoreDataPersistence.swift b/swift-sdk/Internal/IterableCoreDataPersistence.swift index 1770fa36a..9ab99c0cc 100644 --- a/swift-sdk/Internal/IterableCoreDataPersistence.swift +++ b/swift-sdk/Internal/IterableCoreDataPersistence.swift @@ -160,7 +160,13 @@ struct CoreDataPersistenceContext: IterablePersistenceContext { func deleteAllTasks() throws { let taskManagedObjects: [IterableTaskManagedObject] = try CoreDataUtil.findAll(context: managedObjectContext, entity: PersistenceConst.Entity.Task.name) - taskManagedObjects.forEach { managedObjectContext.delete($0) } + taskManagedObjects.forEach { + if !$0.isDeleted { + managedObjectContext.delete($0) + } else { + ITBDebug("task already deleted") + } + } } func countTasks() throws -> Int { diff --git a/swift-sdk/Internal/IterableTaskScheduler.swift b/swift-sdk/Internal/IterableTaskScheduler.swift index 1ce67aee2..f35243548 100644 --- a/swift-sdk/Internal/IterableTaskScheduler.swift +++ b/swift-sdk/Internal/IterableTaskScheduler.swift @@ -54,7 +54,7 @@ class IterableTaskScheduler { do { try persistenceContext.deleteAllTasks() try persistenceContext.save() - } catch let error { + } catch { ITBError("deleteAllTasks: \(error.localizedDescription)") self?.healthMonitor.onDeleteAllTasksError() } diff --git a/swift-sdk/Internal/NetworkHelper.swift b/swift-sdk/Internal/NetworkHelper.swift index 657bfc0e9..af3234330 100644 --- a/swift-sdk/Internal/NetworkHelper.swift +++ b/swift-sdk/Internal/NetworkHelper.swift @@ -28,6 +28,9 @@ extension NetworkError: LocalizedError { } struct NetworkHelper { + static let maxRetryCount = 5 + static let retryDelaySeconds = 2 + static func getData(fromUrl url: URL, usingSession networkSession: NetworkSessionProtocol) -> Pending { let fulfill = Fulfill() @@ -52,8 +55,9 @@ struct NetworkHelper { static func sendRequest(_ request: URLRequest, converter: @escaping (Data) throws -> T?, usingSession networkSession: NetworkSessionProtocol) -> Pending { - #if NETWORK_DEBUG + let requestId = IterableUtil.generateUUID() + #if NETWORK_DEBUG print() print("====================================================>") print("sending request: \(request)") @@ -73,29 +77,65 @@ struct NetworkHelper { #endif let fulfill = Fulfill() - - networkSession.makeRequest(request) { data, response, error in - let result = createResultFromNetworkResponse(data: data, - converter: converter, - response: response, - error: error) - switch result { - case let .success(value): - #if NETWORK_DEBUG - print("request with id: \(requestId) successfully sent, response:") - print(value) - #endif - fulfill.resolve(with: value) - case let .failure(error): + func sendRequestWithRetries(request: URLRequest, requestId: String, retriesLeft: Int) { + networkSession.makeRequest(request) { data, response, error in + let result = createResultFromNetworkResponse(data: data, + converter: converter, + response: response, + error: error) + switch result { + case let .success(value): + handleSuccess(requestId: requestId, value: value) + case let .failure(error): + handleFailure(requestId: requestId, request: request, error: error, retriesLeft: retriesLeft) + } + } + } + + func handleSuccess(requestId: String, value: T) { + #if NETWORK_DEBUG + print("request with id: \(requestId) successfully sent, response:") + print(value) + #endif + fulfill.resolve(with: value) + } + + func handleFailure(requestId: String, request: URLRequest, error: NetworkError, retriesLeft: Int) { + if shouldRetry(error: error, retriesLeft: retriesLeft) { + retryRequest(requestId: requestId, request: request, error: error, retriesLeft: retriesLeft) + } else { #if NETWORK_DEBUG print("request with id: \(requestId) errored") print(error) #endif fulfill.reject(with: error) } + } + func shouldRetry(error: NetworkError, retriesLeft: Int) -> Bool { + return error.httpStatusCode ?? 0 >= 500 && retriesLeft > 0 + } + + func retryRequest(requestId: String, request: URLRequest, error: NetworkError, retriesLeft: Int) { + #if NETWORK_DEBUG + print("retry attempt: \(maxRetryCount-retriesLeft+1) for url: \(request.url?.absoluteString ?? "")") + print(error) + #endif + + var delay: DispatchTimeInterval = .seconds(0) + if retriesLeft <= 3 { + delay = .seconds(retryDelaySeconds) + } + + DispatchQueue.global().asyncAfter(deadline: .now() + delay) { + sendRequestWithRetries(request: request, requestId: requestId, retriesLeft: retriesLeft - 1) + } + } + + sendRequestWithRetries(request: request, requestId: requestId, retriesLeft: maxRetryCount) + return fulfill } diff --git a/swift-sdk/Internal/RequestHandler.swift b/swift-sdk/Internal/RequestHandler.swift index 8623eaad0..913844cef 100644 --- a/swift-sdk/Internal/RequestHandler.swift +++ b/swift-sdk/Internal/RequestHandler.swift @@ -8,7 +8,7 @@ class RequestHandler: RequestHandlerProtocol { init(onlineProcessor: OnlineRequestProcessor, offlineProcessor: OfflineRequestProcessor?, healthMonitor: HealthMonitor?, - offlineMode: Bool = true) { + offlineMode: Bool = false) { ITBInfo() self.onlineProcessor = onlineProcessor self.offlineProcessor = offlineProcessor diff --git a/swift-sdk/Internal/RequestProcessorUtil.swift b/swift-sdk/Internal/RequestProcessorUtil.swift index 797295eaa..da24fe6f1 100644 --- a/swift-sdk/Internal/RequestProcessorUtil.swift +++ b/swift-sdk/Internal/RequestProcessorUtil.swift @@ -18,7 +18,7 @@ struct RequestProcessorUtil { .onError { error in if error.httpStatusCode == 401, error.iterableCode == JsonValue.Code.invalidJwtPayload { ITBError("invalid JWT token, trying again: \(error.reason ?? "")") - authManager?.requestNewAuthToken(hasFailedPriorAuth: true) { _ in + authManager?.requestNewAuthToken(hasFailedPriorAuth: false) { _ in requestProvider().onSuccess { json in reportSuccess(result: result, value: json, successHandler: onSuccess, identifier: identifier) }.onError { error in diff --git a/swift-sdk/IterableAPI.swift b/swift-sdk/IterableAPI.swift index b0f8e39c0..96a8aac27 100644 --- a/swift-sdk/IterableAPI.swift +++ b/swift-sdk/IterableAPI.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers public final class IterableAPI: NSObject { /// The current SDK version - public static let sdkVersion = "6.5.0-alpha1" + public static let sdkVersion = "6.4.16" /// The email of the logged in user that this IterableAPI is using public static var email: String? { @@ -129,12 +129,12 @@ import UIKit // MARK: - SDK - public static func setEmail(_ email: String?, _ authToken: String? = nil) { - implementation?.setEmail(email, authToken: authToken) + public static func setEmail(_ email: String?, _ authToken: String? = nil, _ successHandler: OnSuccessHandler? = nil, _ failureHandler: OnFailureHandler? = nil) { + implementation?.setEmail(email, authToken: authToken, successHandler: successHandler, failureHandler: failureHandler) } - public static func setUserId(_ userId: String?, _ authToken: String? = nil) { - implementation?.setUserId(userId, authToken: authToken) + public static func setUserId(_ userId: String?, _ authToken: String? = nil, _ successHandler: OnSuccessHandler? = nil, _ failureHandler: OnFailureHandler? = nil) { + implementation?.setUserId(userId, authToken: authToken, successHandler: successHandler, failureHandler: failureHandler) } /// Handle a Universal Link diff --git a/swift-sdk/IterableConfig.swift b/swift-sdk/IterableConfig.swift index 20770bf45..c254db7a9 100644 --- a/swift-sdk/IterableConfig.swift +++ b/swift-sdk/IterableConfig.swift @@ -124,4 +124,7 @@ public class IterableConfig: NSObject { /// Set whether the SDK should store in-apps only in memory, or in file storage public var useInMemoryStorageForInApps = false + + /// Sets data region which determines data center and endpoints used by the SDK + public var dataRegion: String = IterableDataRegion.US } diff --git a/swift-sdk/IterableInAppManagerProtocol.swift b/swift-sdk/IterableInAppManagerProtocol.swift index d5a0ac9b2..c4fd53569 100644 --- a/swift-sdk/IterableInAppManagerProtocol.swift +++ b/swift-sdk/IterableInAppManagerProtocol.swift @@ -28,22 +28,49 @@ import Foundation /// Note that this callback is called in addition to calling `IterableCustomActionDelegate` or `IterableUrlDelegate` on the button action. @objc(showMessage:consume:callbackBlock:) func show(message: IterableInAppMessage, consume: Bool, callback: ITBURLCallback?) + /// - parameter message: The message to remove. @objc(removeMessage:) func remove(message: IterableInAppMessage) + /// - parameter message: The message to remove. + /// - parameter successHandler: The callback which returns `success. + /// - parameter failureHandler: The callback which returns `failure. + @objc(removeMessage:successHandler:failureHandler:) func remove(message: IterableInAppMessage, successHandler: OnSuccessHandler?, failureHandler: OnFailureHandler?) + + /// - parameter message: The message to remove. /// - parameter location: The location from where this message was shown. `inbox` or `inApp`. @objc(removeMessage:location:) func remove(message: IterableInAppMessage, location: InAppLocation) + /// - parameter message: The message to remove. + /// - parameter location: The location from where this message was shown. `inbox` or `inApp`. + /// - parameter successHandler: The callback which returns `success. + /// - parameter failureHandler: The callback which returns `failure. + @objc(removeMessage:location:successHandler:failureHandler:) func remove(message: IterableInAppMessage, location: InAppLocation, successHandler: OnSuccessHandler?, failureHandler: OnFailureHandler?) + + /// - parameter message: The message to remove. /// - parameter location: The location from where this message was shown. `inbox` or `inApp`. /// - parameter source: The source of deletion `inboxSwipe` or `deleteButton`.` @objc(removeMessage:location:source:) func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource) + /// - parameter message: The message to remove. + /// - parameter location: The location from where this message was shown. `inbox` or `inApp`. + /// - parameter source: The source of deletion `inboxSwipe` or `deleteButton`.` + /// - parameter successHandler: The callback which returns `success. + /// - parameter failureHandler: The callback which returns `failure. + @objc(removeMessage:location:source:successHandler:failureHandler:) func remove(message: IterableInAppMessage, location: InAppLocation, source: InAppDeleteSource, successHandler: OnSuccessHandler?, failureHandler: OnFailureHandler?) + /// - parameter read: Whether this inbox message was read /// - parameter message: The inbox message @objc(setRead:forMessage:) func set(read: Bool, forMessage message: IterableInAppMessage) + /// - parameter read: Whether this inbox message was read + /// - parameter message: The inbox message + /// - parameter successHandler: The callback which returns `success. + /// - parameter failureHandler: The callback which returns `failure. + @objc(setRead:forMessage:successHandler:failureHandler:) func set(read: Bool, forMessage message: IterableInAppMessage, successHandler: OnSuccessHandler?, failureHandler: OnFailureHandler?) + /// - parameter id: The id of the message /// - returns: IterableInAppMessage with the id, if it exists. @objc(getMessageWithId:) func getMessage(withId id: String) -> IterableInAppMessage? diff --git a/tests/common/MockAuthManager.swift b/tests/common/MockAuthManager.swift new file mode 100644 index 000000000..b492c3df7 --- /dev/null +++ b/tests/common/MockAuthManager.swift @@ -0,0 +1,43 @@ +// +// MockAuthManager.swift +// swift-sdk +// +// Created by HARDIK MASHRU on 08/11/23. +// Copyright © 2023 Iterable. All rights reserved. +// +import Foundation +@testable import IterableSDK + +class MockAuthManager: IterableAuthManagerProtocol { + var shouldRetry = true + var retryWasRequested = false + + func getAuthToken() -> String? { + return "AuthToken" + } + + func resetFailedAuthCount() { + + } + + func requestNewAuthToken(hasFailedPriorAuth: Bool, onSuccess: ((String?) -> Void)?) { + if shouldRetry { + // Simulate the authManager obtaining a new token + retryWasRequested = true + shouldRetry = false + onSuccess?("newAuthToken") + } else { + // Simulate failing to obtain a new token + retryWasRequested = false + onSuccess?(nil) + } + } + + func setNewToken(_ newToken: String) { + + } + + func logoutUser() { + + } +} diff --git a/tests/unit-tests/AuthTests.swift b/tests/unit-tests/AuthTests.swift index eac7136ca..52391d779 100644 --- a/tests/unit-tests/AuthTests.swift +++ b/tests/unit-tests/AuthTests.swift @@ -525,7 +525,9 @@ class AuthTests: XCTestCase { XCTAssertNil(internalAPI.auth.authToken) } - func testAuthTokenRefreshRetryOnlyOnce() { + func testAuthTokenRefreshRetryOnlyOnce() throws { + throw XCTSkip("skipping this test - auth token retries should occur more than once") + let condition1 = expectation(description: "\(#function) - callback not called correctly in some form") condition1.expectedFulfillmentCount = 2 diff --git a/tests/unit-tests/InboxTests.swift b/tests/unit-tests/InboxTests.swift index 37f98c535..79f466015 100644 --- a/tests/unit-tests/InboxTests.swift +++ b/tests/unit-tests/InboxTests.swift @@ -162,12 +162,12 @@ class InboxTests: XCTestCase { let mockInAppFetcher = MockInAppFetcher() let config = IterableConfig() config.logDelegate = AllLogDelegate() - + let internalAPI = InternalIterableAPI.initializeForTesting( config: config, inAppFetcher: mockInAppFetcher ) - + let payload = """ {"inAppMessages": [ @@ -190,19 +190,27 @@ class InboxTests: XCTestCase { ] } """.toJsonDict() - + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalAPI, payload).onSuccess { _ in let messages = internalAPI.inAppManager.getInboxMessages() XCTAssertEqual(messages.count, 2) - internalAPI.inAppManager.remove(message: messages[0], location: .inbox, source: .inboxSwipe) + let messageToRemove = messages[0] + internalAPI.inAppManager.remove( + message: messageToRemove, + location: .inbox, + source: .inboxSwipe, + successHandler: { _ in }, + failureHandler: { _, _ in } + ) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { let newMessages = internalAPI.inAppManager.getInboxMessages() XCTAssertEqual(newMessages.count, 1) expectation1.fulfill() } } - + wait(for: [expectation1], timeout: testExpectationTimeout) } diff --git a/tests/unit-tests/IterableAPIResponseTests.swift b/tests/unit-tests/IterableAPIResponseTests.swift index 67881ac3e..b6b091581 100644 --- a/tests/unit-tests/IterableAPIResponseTests.swift +++ b/tests/unit-tests/IterableAPIResponseTests.swift @@ -100,6 +100,54 @@ class IterableAPIResponseTests: XCTestCase { wait(for: [xpectation], timeout: testExpectationTimeout) } + func testRetryOnInvalidJwtPayload() { + let xpectation = expectation(description: "retry on 401 with invalidJWTPayload") + + // Mock the dependencies and requestProvider for your test + let authManager = MockAuthManager() + + let networkErrorSession = MockNetworkSession() { _ in + MockNetworkSession.MockResponse(statusCode: 401, + data: ["code":"InvalidJwtPayload"].toJsonData(), + delay: 1) + } + + let networkSuccessSession = MockNetworkSession() { _ in + MockNetworkSession.MockResponse(statusCode: 200, + data: ["msg": "success"].toJsonData(), + delay: 1) + } + + let urlErrorRequest = createApiClient(networkSession: networkErrorSession).convertToURLRequest(iterableRequest: IterableRequest.post(PostRequest(path: "", args: nil, body: [:])))! + + + let urlSuccessRequest = createApiClient(networkSession: networkSuccessSession).convertToURLRequest(iterableRequest: IterableRequest.post(PostRequest(path: "", args: nil, body: [:])))! + + let requestProvider: () -> Pending = { + if authManager.retryWasRequested { + return RequestSender.sendRequest(urlSuccessRequest, usingSession: networkSuccessSession) + } + return RequestSender.sendRequest(urlErrorRequest, usingSession: networkErrorSession) + } + + let result = RequestProcessorUtil.sendRequest( + requestProvider: requestProvider, + authManager: authManager, + requestIdentifier: "TestIdentifier" + ) + + result.onSuccess { value in + xpectation.fulfill() + XCTAssert(true) + }.onError { error in + if authManager.retryWasRequested { + xpectation.fulfill() + } + } + + waitForExpectations(timeout: testExpectationTimeout) + } + func testResponseCode401() { // 401 = unauthorized let xpectation = expectation(description: "401") let iterableRequest = IterableRequest.post(PostRequest(path: "", args: nil, body: [:])) @@ -152,6 +200,30 @@ class IterableAPIResponseTests: XCTestCase { wait(for: [xpectation], timeout: testExpectationTimeout) } + func testSendRequestWithRetry() { + let xpectation = expectation(description: "retry on status code >= 500") + + let networkSession = MockNetworkSession { _ in + MockNetworkSession.MockResponse(statusCode: 503, + data: Data(), + delay: 0) + } + + let iterableRequest = IterableRequest.post(PostRequest(path: "", args: nil, body: [:])) + + let apiClient = createApiClient(networkSession: networkSession) + var urlRequest = apiClient.convertToURLRequest(iterableRequest: iterableRequest)! + urlRequest.timeoutInterval = 1 + + RequestSender.sendRequest(urlRequest, usingSession: networkSession).onError { sendError in + xpectation.fulfill() + XCTAssert(sendError.reason!.lowercased().contains("internal server error")) + } + + wait(for: [xpectation], timeout: testExpectationTimeout) + } + + func testNetworkTimeoutResponse() { let xpectation = expectation(description: "timeout network response") diff --git a/tests/unit-tests/IterableAPITests.swift b/tests/unit-tests/IterableAPITests.swift index 4dea77d1e..02177a04b 100644 --- a/tests/unit-tests/IterableAPITests.swift +++ b/tests/unit-tests/IterableAPITests.swift @@ -11,6 +11,8 @@ class IterableAPITests: XCTestCase { private static let apiKey = "zeeApiKey" private static let email = "user@example.com" private static let userId = "testUserId" + private static let apiEndPointUS = "https://api.iterable.com/api/" + private static let apiEndPointEU = "https://api.eu.iterable.com/api/" override func setUp() { super.setUp() @@ -34,6 +36,32 @@ class IterableAPITests: XCTestCase { XCTAssertEqual(internalAPI.apiKey, IterableAPITests.apiKey) } + func testInitializeWithDefaultDataRegion() { + let prodIntegrationName = "the-best-app-ever" + + let config = IterableConfig() + config.pushIntegrationName = prodIntegrationName + config.inAppDisplayInterval = 1.0 + config.dataRegion = IterableDataRegion.US + + let internalAPI = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, config: config) + + XCTAssertEqual(internalAPI.apiEndPointForTest, IterableAPITests.apiEndPointUS) + } + + func testInitializeWithEUDataRegion() { + let prodIntegrationName = "the-best-app-ever" + + let config = IterableConfig() + config.pushIntegrationName = prodIntegrationName + config.inAppDisplayInterval = 1.0 + config.dataRegion = IterableDataRegion.EU + + let internalAPI = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, config: config) + + XCTAssertEqual(internalAPI.apiEndPointForTest, IterableAPITests.apiEndPointEU) + } + func testInitializeCheckEndpoint() { let expectation1 = XCTestExpectation(description: "api endpoint called") @@ -165,6 +193,78 @@ class IterableAPITests: XCTestCase { wait(for: [expectation], timeout: testExpectationTimeout) } + + func testSetEmailWithCallbackSuccess() { + let expectation = XCTestExpectation(description: "Set email with callback success") + + let config = IterableConfig() + let networkSession = MockNetworkSession(statusCode: 200) + let internalAPI = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, config: config, networkSession: networkSession) + + internalAPI.setEmail("test@example.com", successHandler: { success in + XCTAssertNotNil(success) + expectation.fulfill() + }, failureHandler: { _, _ in + XCTFail("Failed to set email") + expectation.fulfill() + }) + internalAPI.register(token: "zeeToken".data(using: .utf8)!) + wait(for: [expectation], timeout: testExpectationTimeout) + } + + func testSetEmailWithCallbackFailure() { + let expectation = XCTestExpectation(description: "Set email with callback failure") + + let config = IterableConfig() + let networkSession = MockNetworkSession(statusCode: 400) + let internalAPI = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, config: config, networkSession: networkSession) + + internalAPI.setEmail("invalid_email", successHandler: { success in + XCTFail("Email should not be set successfully") + expectation.fulfill() + }, failureHandler: { _, error in + XCTAssertNotNil(error) + expectation.fulfill() + }) + internalAPI.register(token: "zeeToken".data(using: .utf8)!) + wait(for: [expectation], timeout: testExpectationTimeout) + } + + func testSetUserIdWithCallbackSuccess() { + let expectation = XCTestExpectation(description: "Set user ID with callback success") + + let config = IterableConfig() + let networkSession = MockNetworkSession(statusCode: 200) + let internalAPI = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, config: config, networkSession: networkSession) + + internalAPI.setUserId("user123", successHandler: { success in + XCTAssertNotNil(success) + expectation.fulfill() + }, failureHandler: { _, _ in + XCTFail("Failed to set user ID") + expectation.fulfill() + }) + internalAPI.register(token: "zeeToken".data(using: .utf8)!) + wait(for: [expectation], timeout: testExpectationTimeout) + } + + func testSetUserIdWithCallbackFailure() { + let expectation = XCTestExpectation(description: "Set user ID with callback failure") + + let config = IterableConfig() + let networkSession = MockNetworkSession(statusCode: 400) + let internalAPI = InternalIterableAPI.initializeForTesting(apiKey: IterableAPITests.apiKey, config: config, networkSession: networkSession) + + internalAPI.setUserId("user123", successHandler: { success in + XCTFail("User ID should not be set successfully") + expectation.fulfill() + }, failureHandler: { _, error in + XCTAssertNotNil(error) + expectation.fulfill() + }) + internalAPI.register(token: "zeeToken".data(using: .utf8)!) + wait(for: [expectation], timeout: testExpectationTimeout) + } func testEmailPersistence() { let internalAPI = InternalIterableAPI.initializeForTesting()