diff --git a/swift-sdk/Core/Constants.swift b/swift-sdk/Core/Constants.swift index ab39eef4f..a5205e60e 100644 --- a/swift-sdk/Core/Constants.swift +++ b/swift-sdk/Core/Constants.swift @@ -316,6 +316,7 @@ enum JsonKey { enum Embedded { static let packageName = "packageName" static let sdkVersion = "SDKVersion" + static let placementIds = "placementIds" } enum Header { diff --git a/swift-sdk/Core/Protocols/IterableEmbeddedManagerProtocol.swift b/swift-sdk/Core/Protocols/IterableEmbeddedManagerProtocol.swift index a38c652f3..d55dadb26 100644 --- a/swift-sdk/Core/Protocols/IterableEmbeddedManagerProtocol.swift +++ b/swift-sdk/Core/Protocols/IterableEmbeddedManagerProtocol.swift @@ -10,7 +10,13 @@ public protocol IterableEmbeddedManagerProtocol { func addUpdateListener(_ listener: IterableEmbeddedUpdateDelegate) func removeUpdateListener(_ listener: IterableEmbeddedUpdateDelegate) - func syncMessages(completion: @escaping () -> Void) + func syncMessages(placementIds: [Int]?, completion: @escaping () -> Void) func handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String) func reset() } + +public extension IterableEmbeddedManagerProtocol { + func syncMessages(completion: @escaping () -> Void) { + syncMessages(placementIds: nil, completion: completion) + } +} diff --git a/swift-sdk/Internal/EmptyEmbeddedManager.swift b/swift-sdk/Internal/EmptyEmbeddedManager.swift index 7ea5d059b..163952cef 100644 --- a/swift-sdk/Internal/EmptyEmbeddedManager.swift +++ b/swift-sdk/Internal/EmptyEmbeddedManager.swift @@ -23,6 +23,10 @@ class EmptyEmbeddedManager: IterableInternalEmbeddedManagerProtocol { func syncMessages(completion: @escaping () -> Void) { + } + + func syncMessages(placementIds: [Int]?, completion: @escaping () -> Void) { + } public func handleEmbeddedClick(message: IterableEmbeddedMessage, buttonIdentifier: String?, clickedUrl: String) { diff --git a/swift-sdk/Internal/IterableEmbeddedManager.swift b/swift-sdk/Internal/IterableEmbeddedManager.swift index 4cc37c3dc..32f834fd1 100644 --- a/swift-sdk/Internal/IterableEmbeddedManager.swift +++ b/swift-sdk/Internal/IterableEmbeddedManager.swift @@ -161,8 +161,8 @@ class IterableEmbeddedManager: NSObject, IterableInternalEmbeddedManagerProtocol syncMessages { } } - private func retrieveEmbeddedMessages(completion: @escaping () -> Void) { - apiClient.getEmbeddedMessages() + private func retrieveEmbeddedMessages(placementIds: [Int]?, completion: @escaping () -> Void) { + apiClient.getEmbeddedMessages(placementIds: placementIds) .onCompletion( receiveValue: { embeddedMessagesPayload in let placements = embeddedMessagesPayload.placements @@ -172,7 +172,18 @@ class IterableEmbeddedManager: NSObject, IterableInternalEmbeddedManagerProtocol fetchedMessagesDict[placement.placementId!] = placement.embeddedMessages } - let processor = EmbeddedMessagingProcessor(currentMessages: self.messages, + let currentMessagesSnapshot: [Int: [IterableEmbeddedMessage]] = self.messageProcessingQueue.sync { + self.messages + } + + if let placementIds, !placementIds.isEmpty { + let requestedPlacementIds = Set(placementIds) + for (placementId, currentMessages) in currentMessagesSnapshot where !requestedPlacementIds.contains(placementId) { + fetchedMessagesDict[placementId] = currentMessages + } + } + + let processor = EmbeddedMessagingProcessor(currentMessages: currentMessagesSnapshot, fetchedMessages: fetchedMessagesDict) self.setMessages(processor) @@ -260,8 +271,12 @@ class IterableEmbeddedManager: NSObject, IterableInternalEmbeddedManagerProtocol extension IterableEmbeddedManager: EmbeddedNotifiable { public func syncMessages(completion: @escaping () -> Void) { - if (enableEmbeddedMessaging) { - retrieveEmbeddedMessages(completion: completion) + syncMessages(placementIds: nil, completion: completion) + } + + public func syncMessages(placementIds: [Int]?, completion: @escaping () -> Void) { + if enableEmbeddedMessaging { + retrieveEmbeddedMessages(placementIds: placementIds, completion: completion) } } } diff --git a/swift-sdk/Internal/api-client/ApiClient.swift b/swift-sdk/Internal/api-client/ApiClient.swift index c7dd3e44c..15cd6ac63 100644 --- a/swift-sdk/Internal/api-client/ApiClient.swift +++ b/swift-sdk/Internal/api-client/ApiClient.swift @@ -300,8 +300,8 @@ extension ApiClient: ApiClientProtocol { // MARK: - Embedded Messaging - func getEmbeddedMessages() -> Pending { - let result = createRequestCreator().flatMap { $0.createGetEmbeddedMessagesRequest() } + func getEmbeddedMessages(placementIds: [Int]?) -> Pending { + let result = createRequestCreator().flatMap { $0.createGetEmbeddedMessagesRequest(placementIds: placementIds) } return send(iterableRequestResult: result) } diff --git a/swift-sdk/Internal/api-client/ApiClientProtocol.swift b/swift-sdk/Internal/api-client/ApiClientProtocol.swift index 32bb717ff..1765fc614 100644 --- a/swift-sdk/Internal/api-client/ApiClientProtocol.swift +++ b/swift-sdk/Internal/api-client/ApiClientProtocol.swift @@ -60,7 +60,7 @@ protocol ApiClientProtocol: AnyObject { func trackConsent(consentTimestamp: Int64, email: String?, userId: String?, isUserKnown: Bool) -> Pending - func getEmbeddedMessages() -> Pending + func getEmbeddedMessages(placementIds: [Int]?) -> Pending @discardableResult func track(embeddedMessageReceived message: IterableEmbeddedMessage) -> Pending @@ -72,3 +72,9 @@ protocol ApiClientProtocol: AnyObject { @discardableResult func track(embeddedSession: IterableEmbeddedSession) -> Pending } + +extension ApiClientProtocol { + func getEmbeddedMessages() -> Pending { + getEmbeddedMessages(placementIds: nil) + } +} diff --git a/swift-sdk/Internal/api-client/Request/RequestCreator.swift b/swift-sdk/Internal/api-client/Request/RequestCreator.swift index 9a2785b3e..fb2fd1620 100644 --- a/swift-sdk/Internal/api-client/Request/RequestCreator.swift +++ b/swift-sdk/Internal/api-client/Request/RequestCreator.swift @@ -500,7 +500,7 @@ struct RequestCreator { // MARK: - Embedded Messaging Request Calls - func createGetEmbeddedMessagesRequest() -> Result { + func createGetEmbeddedMessagesRequest(placementIds: [Int]?) -> Result { if case .none = auth.emailOrUserId { ITBError(Self.authMissingMessage) return .failure(IterableError.general(description: Self.authMissingMessage)) @@ -513,10 +513,28 @@ struct RequestCreator { if let packageName = Bundle.main.appPackageName { args[JsonKey.Embedded.packageName] = packageName } - setCurrentUser(inDict: &args) - - return .success(.get(createGetRequest(forPath: Const.Path.getEmbeddedMessages, withArgs: args as! [String: String]))) + + let placementIdQueryItems: [URLQueryItem] + if let placementIds, !placementIds.isEmpty { + placementIdQueryItems = placementIds.map { URLQueryItem(name: JsonKey.Embedded.placementIds, value: String($0)) } + } else { + placementIdQueryItems = [] + } + + let argsQueryItems: [URLQueryItem] = args.compactMap { key, value in + guard let key = key as? String, let value = value as? String else { return nil } + if key == JsonKey.Embedded.placementIds { return nil } + return URLQueryItem(name: key, value: value) + } + + var components = URLComponents() + components.path = Const.Path.getEmbeddedMessages + components.queryItems = argsQueryItems + placementIdQueryItems + let query = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B") + let pathWithQuery = components.path + (query.map { "?\($0)" } ?? "") + + return .success(.get(GetRequest(path: pathWithQuery, args: nil))) } func createEmbeddedMessageReceivedRequest(_ message: IterableEmbeddedMessage) -> Result { diff --git a/tests/unit-tests/BlankApiClient.swift b/tests/unit-tests/BlankApiClient.swift index 82df13157..a2d2717c7 100644 --- a/tests/unit-tests/BlankApiClient.swift +++ b/tests/unit-tests/BlankApiClient.swift @@ -114,7 +114,7 @@ class BlankApiClient: ApiClientProtocol { Pending() } - func getEmbeddedMessages() -> Pending { + func getEmbeddedMessages(placementIds: [Int]?) -> Pending { Pending() } diff --git a/tests/unit-tests/EmbeddedManagerTests.swift b/tests/unit-tests/EmbeddedManagerTests.swift index ed5ee7c7f..01313a771 100644 --- a/tests/unit-tests/EmbeddedManagerTests.swift +++ b/tests/unit-tests/EmbeddedManagerTests.swift @@ -109,6 +109,33 @@ final class EmbeddedManagerTests: XCTestCase { wait(for: [syncMessagesExpectation, delegateExpectation, syncSuccessExpectation], timeout: 2) } + + func testSyncMessagesWithPlacementIdsDoesNotClearOtherPlacements() { + let mockApiClient = MockApiClient() + mockApiClient.populateMessages([ + 1: [IterableEmbeddedMessage(messageId: "1a", placementId: 1)], + 2: [IterableEmbeddedMessage(messageId: "2a", placementId: 2)], + ]) + + let manager = IterableEmbeddedManager(apiClient: mockApiClient, + urlDelegate: nil, + customActionDelegate: nil, + urlOpener: MockUrlOpener(), + allowedProtocols: [], + enableEmbeddedMessaging: true) + + manager.syncMessages { } + XCTAssertEqual(manager.getMessages(for: 2).map { $0.metadata.messageId }, ["2a"]) + + // Update only placement 1 on the "server", then request only that placement. + mockApiClient.populateMessages([ + 1: [IterableEmbeddedMessage(messageId: "1b", placementId: 1)], + ]) + manager.syncMessages(placementIds: [1]) { } + + XCTAssertEqual(manager.getMessages(for: 1).map { $0.metadata.messageId }, ["1b"]) + XCTAssertEqual(manager.getMessages(for: 2).map { $0.metadata.messageId }, ["2a"]) + } func testManagerReset() { let syncMessagesExpectation = expectation(description: "syncMessages should complete") @@ -392,16 +419,19 @@ final class EmbeddedManagerTests: XCTestCase { invalidApiKey = true } - override func getEmbeddedMessages() -> IterableSDK.Pending { + override func getEmbeddedMessages(placementIds: [Int]?) -> IterableSDK.Pending { if invalidApiKey { return FailPending(error: IterableSDK.SendRequestError(reason: "Invalid API Key")) } if newMessages { var placements: [Placement] = [] + let requested = Set(placementIds ?? []) for (placementId, messages) in mockMessages { - let placement = Placement(placementId: placementId, embeddedMessages: messages) - placements.append(placement) + if placementIds == nil || requested.contains(placementId) { + let placement = Placement(placementId: placementId, embeddedMessages: messages) + placements.append(placement) + } } let payload = PlacementsPayload(placements: placements) diff --git a/tests/unit-tests/RequestCreatorTests.swift b/tests/unit-tests/RequestCreatorTests.swift index 3d4c97ad3..de5821d0f 100644 --- a/tests/unit-tests/RequestCreatorTests.swift +++ b/tests/unit-tests/RequestCreatorTests.swift @@ -177,6 +177,58 @@ class RequestCreatorTests: XCTestCase { XCTAssertEqual(args[JsonKey.InApp.count], inAppMessageRequestCount.stringValue) XCTAssertEqual(args[JsonKey.systemVersion], UIDevice.current.systemVersion) } + + func testGetEmbeddedMessagesRequestFailure() { + let auth = Auth(userId: nil, email: nil, authToken: nil, userIdUnknownUser: nil) + let requestCreator = RequestCreator(auth: auth, deviceMetadata: deviceMetadata) + + let failingRequest = requestCreator.createGetEmbeddedMessagesRequest(placementIds: nil) + + if let _ = try? failingRequest.get() { + XCTFail("request succeeded despite userId and email being nil") + } + } + + func testGetEmbeddedMessagesRequest() { + let request = createRequestCreator().createGetEmbeddedMessagesRequest(placementIds: nil) + let urlRequest = convertToUrlRequest(request) + + TestUtils.validateHeader(urlRequest, apiKey) + TestUtils.validate(request: urlRequest, requestType: .get, apiEndPoint: Endpoint.api, path: Const.Path.getEmbeddedMessages) + + guard let url = urlRequest.url, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + XCTFail("could not create URLComponents from request url") + return + } + + let queryItems = urlComponents.queryItems ?? [] + XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.email })?.value, auth.email) + XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.Embedded.packageName })?.value, Bundle.main.appPackageName) + XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.systemVersion })?.value, UIDevice.current.systemVersion) + XCTAssertTrue(queryItems.filter { $0.name == JsonKey.Embedded.placementIds }.isEmpty) + } + + func testGetEmbeddedMessagesRequestWithPlacementIds() { + let request = createRequestCreator().createGetEmbeddedMessagesRequest(placementIds: [1, 2, 3]) + let urlRequest = convertToUrlRequest(request) + + TestUtils.validateHeader(urlRequest, apiKey) + TestUtils.validate(request: urlRequest, requestType: .get, apiEndPoint: Endpoint.api, path: Const.Path.getEmbeddedMessages) + + guard let url = urlRequest.url, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + XCTFail("could not create URLComponents from request url") + return + } + + let queryItems = urlComponents.queryItems ?? [] + XCTAssertEqual(queryItems.first(where: { $0.name == JsonKey.email })?.value, auth.email) + + let placementIds = queryItems + .filter { $0.name == JsonKey.Embedded.placementIds } + .compactMap(\.value) + + XCTAssertEqual(placementIds, ["1", "2", "3"]) + } func testTrackEventRequest() { let eventName = "dsfsdf"