From 6b06197d6b4b9fe20318e7697e83297c2977ebe1 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Thu, 27 Mar 2025 17:52:38 +0000 Subject: [PATCH 1/3] [MOB-9339] Potential fix for recalled campaigns --- .../in-app/InAppManager+Functions.swift | 8 ++ tests/unit-tests/InAppTests.swift | 125 ++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/swift-sdk/Internal/in-app/InAppManager+Functions.swift b/swift-sdk/Internal/in-app/InAppManager+Functions.swift index 8e0c93fad..7bf213158 100644 --- a/swift-sdk/Internal/in-app/InAppManager+Functions.swift +++ b/swift-sdk/Internal/in-app/InAppManager+Functions.swift @@ -132,6 +132,14 @@ struct MessagesObtainedHandler { var messagesOverwritten = 0 var newMessagesMap = OrderedDictionary() + + // Mark messages that have been removed from the server response as consumed + // This ensures recalled campaigns won't be shown + removedMessages.forEach { message in + message.consumed = true + newMessagesMap[message.messageId] = message + } + messages.forEach { serverMessage in let messageId = serverMessage.messageId if let existingMessage = messagesMap[messageId] { diff --git a/tests/unit-tests/InAppTests.swift b/tests/unit-tests/InAppTests.swift index 74bdd6905..d8a60844c 100644 --- a/tests/unit-tests/InAppTests.swift +++ b/tests/unit-tests/InAppTests.swift @@ -1849,6 +1849,131 @@ class InAppTests: XCTestCase { wait(for: [expectation1], timeout: testExpectationTimeout) } + func testRecalledMessagesAreConsumed() { + let expectation1 = expectation(description: "messages synced initially") + let expectation2 = expectation(description: "messages synced after recall") + + let mockInAppFetcher = MockInAppFetcher() + + let config = IterableConfig() + let internalApi = InternalIterableAPI.initializeForTesting( + config: config, + inAppFetcher: mockInAppFetcher + ) + + // First, mock some messages on the server + let initialPayload = """ + {"inAppMessages": + [ + { + "saveToInbox": true, + "content": {"contentType": "html", "inAppDisplaySettings": {"bottom": {"displayOption": "AutoExpand"}, "backgroundAlpha": 0.5, "left": {"percentage": 60}, "right": {"percentage": 60}, "top": {"displayOption": "AutoExpand"}}, "html": "Click Here"}, + "trigger": {"type": "never"}, + "messageId": "msg1", + "campaignId": 1, + "customPayload": {"title": "Message 1", "date": "2018-11-14T14:00:00:00.32Z"} + }, + { + "saveToInbox": true, + "content": {"contentType": "html", "inAppDisplaySettings": {"bottom": {"displayOption": "AutoExpand"}, "backgroundAlpha": 0.5, "left": {"percentage": 60}, "right": {"percentage": 60}, "top": {"displayOption": "AutoExpand"}}, "html": "Click Here"}, + "trigger": {"type": "never"}, + "messageId": "msg2", + "campaignId": 2, + "customPayload": {"title": "Message 2", "date": "2018-11-14T14:00:00:00.32Z"} + } + ] + } + """.toJsonDict() + + // Initial sync with both messages + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, initialPayload).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("internalApi is nil") + return + } + + let messages = internalApi.inAppManager.getMessages() + XCTAssertEqual(messages.count, 2, "Should have 2 messages initially") + XCTAssertTrue(messages.contains(where: { $0.messageId == "msg1" }), "Should contain msg1") + XCTAssertTrue(messages.contains(where: { $0.messageId == "msg2" }), "Should contain msg2") + + expectation1.fulfill() + + // Now simulate a recall by removing msg1 from the server response + let recallPayload = """ + {"inAppMessages": + [ + { + "saveToInbox": true, + "content": {"contentType": "html", "inAppDisplaySettings": {"bottom": {"displayOption": "AutoExpand"}, "backgroundAlpha": 0.5, "left": {"percentage": 60}, "right": {"percentage": 60}, "top": {"displayOption": "AutoExpand"}}, "html": "Click Here"}, + "trigger": {"type": "never"}, + "messageId": "msg2", + "campaignId": 2, + "customPayload": {"title": "Message 2", "date": "2018-11-14T14:00:00:00.32Z"} + } + ] + } + """.toJsonDict() + + // Second sync with msg1 removed (simulating recall) + mockInAppFetcher.mockInAppPayloadFromServer(internalApi: internalApi, recallPayload).onSuccess { [weak internalApi] _ in + guard let internalApi = internalApi else { + XCTFail("internalApi is nil") + return + } + + let messagesAfterRecall = internalApi.inAppManager.getMessages() + XCTAssertEqual(messagesAfterRecall.count, 1, "Should have only 1 valid message after recall") + XCTAssertEqual(messagesAfterRecall.first?.messageId, "msg2", "Only msg2 should remain active") + + // Check that msg1 is still in the internal map but marked as consumed + let msg1 = internalApi.inAppManager.getMessage(withId: "msg1") + XCTAssertNotNil(msg1, "msg1 should still exist internally") + XCTAssertTrue(msg1?.consumed ?? false, "msg1 should be marked as consumed") + + expectation2.fulfill() + } + } + + wait(for: [expectation1, expectation2], timeout: testExpectationTimeout) + } + + func testInboxChangedIsCalledWhenInAppIsRemovedInServer() { + let expectation1 = expectation(description: "testInboxChangedIsCalledWhenInAppIsRemovedInServer") + + let notification = """ + { + "itbl" : { + "messageId" : "background_notification", + "isGhostPush" : true + }, + "notificationType" : "InAppRemove", + "messageId" : "messageId" + } + """.toJsonDict() + + let mockNotificationCenter = MockNotificationCenter() + let reference = mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in + expectation1.fulfill() + } + + XCTAssertNotNil(reference) + + let config = IterableConfig() + let internalApi = InternalIterableAPI.initializeForTesting(config: config, notificationCenter: mockNotificationCenter) + + let appIntegrationInternal = InternalIterableAppIntegration(tracker: internalApi, + urlDelegate: config.urlDelegate, + customActionDelegate: config.customActionDelegate, + urlOpener: MockUrlOpener(), + inAppNotifiable: internalApi.inAppManager, + embeddedNotifiable: internalApi.embeddedManager) + + appIntegrationInternal.application(MockApplicationStateProvider(applicationState: .background), didReceiveRemoteNotification: notification, fetchCompletionHandler: nil) + + wait(for: [expectation1], timeout: testExpectationTimeout) + } + } extension IterableInAppTrigger { From f7588cdd4163255c5adfc7ced2fc35b0cfb0aa78 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Thu, 27 Mar 2025 18:00:30 +0000 Subject: [PATCH 2/3] [MOB-9339] Make sure message map gets updated --- swift-sdk/Internal/in-app/InAppManager+Functions.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/swift-sdk/Internal/in-app/InAppManager+Functions.swift b/swift-sdk/Internal/in-app/InAppManager+Functions.swift index 7bf213158..b580d03db 100644 --- a/swift-sdk/Internal/in-app/InAppManager+Functions.swift +++ b/swift-sdk/Internal/in-app/InAppManager+Functions.swift @@ -136,8 +136,9 @@ struct MessagesObtainedHandler { // Mark messages that have been removed from the server response as consumed // This ensures recalled campaigns won't be shown removedMessages.forEach { message in - message.consumed = true - newMessagesMap[message.messageId] = message + var mutableMessage = message + mutableMessage.consumed = true + newMessagesMap[message.messageId] = mutableMessage } messages.forEach { serverMessage in From eeafdb3112983e4e8152ad7c3d847df707f10737 Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Thu, 27 Mar 2025 18:15:13 +0000 Subject: [PATCH 3/3] [MOB-9339] Make sure message map gets updated --- tests/unit-tests/InAppTests.swift | 36 ------------------------------- 1 file changed, 36 deletions(-) diff --git a/tests/unit-tests/InAppTests.swift b/tests/unit-tests/InAppTests.swift index d8a60844c..58139b2c8 100644 --- a/tests/unit-tests/InAppTests.swift +++ b/tests/unit-tests/InAppTests.swift @@ -1937,42 +1937,6 @@ class InAppTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: testExpectationTimeout) } - - func testInboxChangedIsCalledWhenInAppIsRemovedInServer() { - let expectation1 = expectation(description: "testInboxChangedIsCalledWhenInAppIsRemovedInServer") - - let notification = """ - { - "itbl" : { - "messageId" : "background_notification", - "isGhostPush" : true - }, - "notificationType" : "InAppRemove", - "messageId" : "messageId" - } - """.toJsonDict() - - let mockNotificationCenter = MockNotificationCenter() - let reference = mockNotificationCenter.addCallback(forNotification: .iterableInboxChanged) { _ in - expectation1.fulfill() - } - - XCTAssertNotNil(reference) - - let config = IterableConfig() - let internalApi = InternalIterableAPI.initializeForTesting(config: config, notificationCenter: mockNotificationCenter) - - let appIntegrationInternal = InternalIterableAppIntegration(tracker: internalApi, - urlDelegate: config.urlDelegate, - customActionDelegate: config.customActionDelegate, - urlOpener: MockUrlOpener(), - inAppNotifiable: internalApi.inAppManager, - embeddedNotifiable: internalApi.embeddedManager) - - appIntegrationInternal.application(MockApplicationStateProvider(applicationState: .background), didReceiveRemoteNotification: notification, fetchCompletionHandler: nil) - - wait(for: [expectation1], timeout: testExpectationTimeout) - } }