From cb312776a4ed865f791ba7639235312096062d28 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Mon, 15 Dec 2025 11:43:06 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=A7Added=20placement=20IDs=20as=20?= =?UTF-8?q?list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- swift-sdk/Core/Constants.swift | 1 + .../IterableEmbeddedManagerProtocol.swift | 8 +++++- swift-sdk/Internal/EmptyEmbeddedManager.swift | 4 +++ .../Internal/IterableEmbeddedManager.swift | 25 +++++++++++++++---- swift-sdk/Internal/api-client/ApiClient.swift | 4 +-- .../api-client/ApiClientProtocol.swift | 8 +++++- .../api-client/Request/RequestCreator.swift | 6 ++++- tests/unit-tests/BlankApiClient.swift | 2 +- tests/unit-tests/EmbeddedManagerTests.swift | 9 ++++--- 9 files changed, 53 insertions(+), 14 deletions(-) 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..8d538fa98 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)) @@ -514,6 +514,10 @@ struct RequestCreator { args[JsonKey.Embedded.packageName] = packageName } + if let placementIds, !placementIds.isEmpty { + args[JsonKey.Embedded.placementIds] = placementIds.map(String.init).joined(separator: ",") + } + setCurrentUser(inDict: &args) return .success(.get(createGetRequest(forPath: Const.Path.getEmbeddedMessages, withArgs: args as! [String: String]))) 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..3e4f64688 100644 --- a/tests/unit-tests/EmbeddedManagerTests.swift +++ b/tests/unit-tests/EmbeddedManagerTests.swift @@ -392,16 +392,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) From 45c25e270ce33f0d78aeacc72e594e9578e88ec0 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Mon, 15 Dec 2025 15:24:51 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A7=AA=20Added=20more=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit-tests/EmbeddedManagerTests.swift | 27 +++++++++++++ tests/unit-tests/RequestCreatorTests.swift | 45 +++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/tests/unit-tests/EmbeddedManagerTests.swift b/tests/unit-tests/EmbeddedManagerTests.swift index 3e4f64688..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") diff --git a/tests/unit-tests/RequestCreatorTests.swift b/tests/unit-tests/RequestCreatorTests.swift index 3d4c97ad3..c00305ec6 100644 --- a/tests/unit-tests/RequestCreatorTests.swift +++ b/tests/unit-tests/RequestCreatorTests.swift @@ -177,6 +177,51 @@ 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 case let .success(.get(getRequest)) = request, let args = getRequest.args else { + XCTFail("could not unwrap to a get request and its arguments") + return + } + + XCTAssertEqual(args[JsonKey.email], auth.email) + XCTAssertEqual(args[JsonKey.Embedded.packageName], Bundle.main.appPackageName) + XCTAssertEqual(args[JsonKey.systemVersion], UIDevice.current.systemVersion) + XCTAssertNil(args[JsonKey.Embedded.placementIds]) + } + + 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 case let .success(.get(getRequest)) = request, let args = getRequest.args else { + XCTFail("could not unwrap to a get request and its arguments") + return + } + + XCTAssertEqual(args[JsonKey.email], auth.email) + XCTAssertEqual(args[JsonKey.Embedded.placementIds], "1,2,3") + } func testTrackEventRequest() { let eventName = "dsfsdf" From 2b3be47cd0e714db3bec2d448719d82ed86a7552 Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Wed, 17 Dec 2025 16:39:00 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=A7Fixed=20wrong=20parameter=20que?= =?UTF-8?q?ry=20building.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api-client/Request/RequestCreator.swift | 26 ++++++++++---- tests/unit-tests/RequestCreatorTests.swift | 35 +++++++++++-------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/swift-sdk/Internal/api-client/Request/RequestCreator.swift b/swift-sdk/Internal/api-client/Request/RequestCreator.swift index 8d538fa98..fb2fd1620 100644 --- a/swift-sdk/Internal/api-client/Request/RequestCreator.swift +++ b/swift-sdk/Internal/api-client/Request/RequestCreator.swift @@ -513,14 +513,28 @@ struct RequestCreator { if let packageName = Bundle.main.appPackageName { args[JsonKey.Embedded.packageName] = packageName } - + setCurrentUser(inDict: &args) + + let placementIdQueryItems: [URLQueryItem] if let placementIds, !placementIds.isEmpty { - args[JsonKey.Embedded.placementIds] = placementIds.map(String.init).joined(separator: ",") + placementIdQueryItems = placementIds.map { URLQueryItem(name: JsonKey.Embedded.placementIds, value: String($0)) } + } else { + placementIdQueryItems = [] } - - setCurrentUser(inDict: &args) - - return .success(.get(createGetRequest(forPath: Const.Path.getEmbeddedMessages, withArgs: args as! [String: String]))) + + 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/RequestCreatorTests.swift b/tests/unit-tests/RequestCreatorTests.swift index c00305ec6..de5821d0f 100644 --- a/tests/unit-tests/RequestCreatorTests.swift +++ b/tests/unit-tests/RequestCreatorTests.swift @@ -195,16 +195,17 @@ class RequestCreatorTests: XCTestCase { TestUtils.validateHeader(urlRequest, apiKey) TestUtils.validate(request: urlRequest, requestType: .get, apiEndPoint: Endpoint.api, path: Const.Path.getEmbeddedMessages) - - guard case let .success(.get(getRequest)) = request, let args = getRequest.args else { - XCTFail("could not unwrap to a get request and its arguments") + + guard let url = urlRequest.url, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + XCTFail("could not create URLComponents from request url") return } - - XCTAssertEqual(args[JsonKey.email], auth.email) - XCTAssertEqual(args[JsonKey.Embedded.packageName], Bundle.main.appPackageName) - XCTAssertEqual(args[JsonKey.systemVersion], UIDevice.current.systemVersion) - XCTAssertNil(args[JsonKey.Embedded.placementIds]) + + 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() { @@ -213,14 +214,20 @@ class RequestCreatorTests: XCTestCase { TestUtils.validateHeader(urlRequest, apiKey) TestUtils.validate(request: urlRequest, requestType: .get, apiEndPoint: Endpoint.api, path: Const.Path.getEmbeddedMessages) - - guard case let .success(.get(getRequest)) = request, let args = getRequest.args else { - XCTFail("could not unwrap to a get request and its arguments") + + guard let url = urlRequest.url, let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + XCTFail("could not create URLComponents from request url") return } - - XCTAssertEqual(args[JsonKey.email], auth.email) - XCTAssertEqual(args[JsonKey.Embedded.placementIds], "1,2,3") + + 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() {