diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 95c0432cd..e5f5cb53f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */; }; 3C2DB2F12DE6CB5E0006B905 /* OneSignalBadgeHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C2DB2EF2DE6CB5E0006B905 /* OneSignalBadgeHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C2DB2F22DE6CB5E0006B905 /* OneSignalBadgeHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C2DB2F02DE6CB5E0006B905 /* OneSignalBadgeHelpers.m */; }; + 3C30FE362F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */; }; 3C3D34E92E95EAA5006A2924 /* LiveActivityConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */; }; 3C3D8D782E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */; }; 3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; }; @@ -1310,6 +1311,7 @@ 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDelta.swift; sourceTree = ""; }; 3C2DB2EF2DE6CB5E0006B905 /* OneSignalBadgeHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalBadgeHelpers.h; sourceTree = ""; }; 3C2DB2F02DE6CB5E0006B905 /* OneSignalBadgeHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalBadgeHelpers.m; sourceTree = ""; }; + 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyTriggerTrackingTests.swift; sourceTree = ""; }; 3C3D34E82E95EAA5006A2924 /* LiveActivityConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityConstants.swift; sourceTree = ""; }; 3C3D8D772E92DB7500C3E977 /* OSLiveActivityViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLiveActivityViewExtensions.swift; sourceTree = ""; }; 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = ""; }; @@ -2168,6 +2170,7 @@ 3C01519B2C2E29F90079E076 /* IAMRequestTests.m */, 3C7021E82ECF0CF4001768C6 /* IAMIntegrationTests.swift */, 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */, + 3C30FE352F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift */, 3CB35FCA2F0FA20B000E6E0F /* OSMessagingControllerUserStateTests.swift */, 3C7021E72ECF0CF3001768C6 /* OneSignalInAppMessagesTests-Bridging-Header.h */, ); @@ -4313,6 +4316,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3C30FE362F21FBE1001B9C25 /* EarlyTriggerTrackingTests.swift in Sources */, 3CAA4BB72F0BAFBA00A16682 /* TriggerTests.swift in Sources */, 3C7021E92ECF0CF4001768C6 /* IAMIntegrationTests.swift in Sources */, 3C01519C2C2E29F90079E076 /* IAMRequestTests.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m index d90f35d0a..45e14b641 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m @@ -149,6 +149,12 @@ @interface OSMessagingController () /// set when we attempt getInAppMessagesFromServer and no onesignal ID is available yet @property (strong, nonatomic, nullable) NSString *shouldFetchOnUserChangeWithSubscriptionID; +/// Tracks whether the first IAM fetch has completed since this cold start +@property (nonatomic) BOOL hasCompletedFirstFetch; + +/// Tracks trigger keys added early on cold start (before first fetch completes), for redisplay logic +@property (strong, nonatomic, nonnull) NSMutableSet *earlySessionTriggers; + @end @implementation OSMessagingController @@ -218,6 +224,8 @@ - (instancetype)init { self.messageDisplayQueue = [NSMutableArray new]; self.clickListeners = [NSMutableArray new]; self.lifecycleListeners = [NSMutableArray new]; + self.hasCompletedFirstFetch = NO; + self.earlySessionTriggers = [NSMutableSet new]; let standardUserDefaults = OneSignalUserDefaults.initStandard; @@ -404,6 +412,23 @@ - (void)updateInAppMessagesFromServer:(NSArray *)newMe self.messages = newMessages; self.calledLoadTags = NO; [self resetRedisplayMessagesBySession]; + + // Apply isTriggerChanged for messages that match triggers added too early on cold start + if (self.earlySessionTriggers.count > 0) { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"Processing triggers added early on cold start: %@", self.earlySessionTriggers]]; + for (OSInAppMessageInternal *message in self.messages) { + if ([self.redisplayedInAppMessages objectForKey:message.messageId] && + [self.triggerController hasSharedTriggers:message newTriggersKeys:self.earlySessionTriggers.allObjects]) { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"Setting isTriggerChanged=YES for message %@", message]]; + message.isTriggerChanged = YES; + } + } + [self.earlySessionTriggers removeAllObjects]; + } + + // Mark that first fetch has completed + self.hasCompletedFirstFetch = YES; + [self evaluateMessages]; [self deleteOldRedisplayedInAppMessages]; } @@ -806,6 +831,13 @@ - (void)evaluateRedisplayedInAppMessages:(NSArray *)newTriggersKeys #pragma mark Trigger Methods - (void)addTriggers:(NSDictionary *)triggers { [self evaluateRedisplayedInAppMessages:triggers.allKeys]; + + // Track triggers added early on cold start (before first fetch completes) for redisplay logic + if (!self.hasCompletedFirstFetch) { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"Tracking triggers added early on cold start: %@", triggers]]; + [self.earlySessionTriggers addObjectsFromArray:triggers.allKeys]; + } + [self.triggerController addTriggers:triggers]; } diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift new file mode 100644 index 000000000..474e4d5a7 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift @@ -0,0 +1,456 @@ +/* + Modified MIT License + + Copyright 2025 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalOSCore +import OneSignalCoreMocks +import OneSignalOSCoreMocks +import OneSignalUserMocks +import OneSignalInAppMessagesMocks +// @testable import OneSignalUser +// @testable import OneSignalInAppMessages + +/** + Tests for early trigger tracking functionality. + + These tests verify that in-app messages can be displayed on cold starts when their + triggers are added very early (before IAM fetch completes). + */ +final class EarlyTriggerTrackingTests: XCTestCase { + + private let testSubscriptionId = "test-subscription-id-12345" + private let testOneSignalId = "test-onesignal-id-12345" + private let testAppId = "test-app-id" + + override func setUpWithError() throws { + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + OSConsistencyManager.shared.reset() + OSMessagingController.removeInstance() + + // Set up basic configuration + OneSignalConfigManager.setAppId(testAppId) + OneSignalLog.setLogLevel(.LL_VERBOSE) + } + + override func tearDownWithError() throws { + OSMessagingController.removeInstance() + } + + /** + Test that hasCompletedFirstFetch is set to true after the first fetch completes. + + Scenario: + - Fresh start with no previous fetch + - First IAM fetch completes + + Expected: + - hasCompletedFirstFetch changes from false to true + */ + func testHasCompletedFirstFetch_isSetAfterFirstFetch() throws { + /* Setup */ + let client = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(client) + OSMessagingController.start() + let controller = OSMessagingController.sharedInstance() + + // Verify initial state + XCTAssertFalse(controller.hasCompletedFirstFetch) + + // Set up mock responses + MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId) + ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId) + + let response = IAMTestHelpers.testFetchMessagesResponse(messages: []) + client.setMockResponseForRequest( + request: "", + response: response) + + /* Execute */ + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Verify */ + XCTAssertTrue(controller.hasCompletedFirstFetch) + } + + /** + Test that triggers added before the first fetch completes are tracked in earlySessionTriggers. + + Scenario: + - Cold start, no IAM fetch has completed yet + - Triggers are added very early in the app lifecycle + + Expected: + - hasCompletedFirstFetch is false + - Triggers are added to earlySessionTriggers + */ + func testTriggersAddedBeforeFirstFetch_areTrackedInEarlySessionTriggers() throws { + /* Setup */ + let client = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(client) + OSMessagingController.start() + let controller = OSMessagingController.sharedInstance() + + // Verify initial state + XCTAssertFalse(controller.hasCompletedFirstFetch) + XCTAssertEqual(controller.earlySessionTriggers.count, 0) + + /* Execute */ + // Add triggers before first fetch completes + controller.addTriggers(["trigger1": "value1", "trigger2": "value2"]) + + /* Verify */ + XCTAssertFalse(controller.hasCompletedFirstFetch) + XCTAssertEqual(controller.earlySessionTriggers.count, 2) + XCTAssertTrue(controller.earlySessionTriggers.contains("trigger1")) + XCTAssertTrue(controller.earlySessionTriggers.contains("trigger2")) + } + + /** + Test that multiple calls to addTriggers before first fetch accumulate in earlySessionTriggers. + + Scenario: + - Multiple trigger additions before first fetch completes + + Expected: + - All trigger keys are accumulated in earlySessionTriggers + */ + func testMultipleTriggersAddedBeforeFirstFetch_areAllTracked() throws { + /* Setup */ + let client = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(client) + OSMessagingController.start() + let controller = OSMessagingController.sharedInstance() + + /* Execute */ + // Add triggers in multiple calls + controller.addTriggers(["trigger1": "value1"]) + controller.addTriggers(["trigger2": "value2"]) + controller.addTriggers(["trigger3": "value3"]) + + /* Verify */ + XCTAssertFalse(controller.hasCompletedFirstFetch, ) + XCTAssertEqual(controller.earlySessionTriggers.count, 3) + XCTAssertTrue(controller.earlySessionTriggers.contains("trigger1")) + XCTAssertTrue(controller.earlySessionTriggers.contains("trigger2")) + XCTAssertTrue(controller.earlySessionTriggers.contains("trigger3")) + } + + /** + Test that triggers added after the first fetch completes are NOT tracked in earlySessionTriggers. + + Scenario: + - First IAM fetch has completed + - New triggers are added + + Expected: + - hasCompletedFirstFetch is true + - Triggers are NOT added to earlySessionTriggers + */ + func testTriggersAddedAfterFirstFetch_areNotTrackedInEarlySessionTriggers() throws { + /* Setup */ + let client = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(client) + OSMessagingController.start() + let controller = OSMessagingController.sharedInstance() + + // Set up mock responses + MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId) + ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId) + + // Set up IAM fetch response with an empty message list + let response = IAMTestHelpers.testFetchMessagesResponse(messages: []) + client.setMockResponseForRequest( + request: "", + response: response) + + // Start the SDK and trigger first fetch + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2.0) + + // Verify first fetch completed + XCTAssertTrue(controller.hasCompletedFirstFetch) + XCTAssertEqual(controller.earlySessionTriggers.count, 0) + + /* Execute */ + // Add triggers after first fetch + controller.addTriggers(["lateAction": "value"]) + + /* Verify */ + XCTAssertTrue(controller.hasCompletedFirstFetch) + XCTAssertEqual(controller.earlySessionTriggers.count, 0) + + // Verify the triggers were still added to the trigger controller + XCTAssertTrue(controller.triggerController.messageMatchesTriggers( + OSInAppMessageInternal.instance(withJson: IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "lateAction", + triggerId: "test_id", + type: 2, // equal + value: "value" + ))! + )) + } + + /** + Test that messages matching early triggers get isTriggerChanged flag set when received from server. + + Scenario: + - Triggers are added before first fetch + - IAM fetch completes with messages that were previously shown (in redisplayedInAppMessages) + - Some messages match the early triggers + + Expected: + - Messages matching early triggers have isTriggerChanged = true + - Messages not matching early triggers have isTriggerChanged = false + - earlySessionTriggers is cleared after processing + - hasCompletedFirstFetch is true after fetch + */ + func testMessagesMatchingEarlyTriggers_getIsTriggerChangedFlag() throws { + /* Setup */ + let client = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(client) + OSMessagingController.start() + let controller = OSMessagingController.sharedInstance() + + // Add triggers before first fetch + controller.addTriggers(["loginAction": "complete", "sessionStart": "true"]) + + // Verify triggers were tracked + XCTAssertEqual(controller.earlySessionTriggers.count, 2) + XCTAssertFalse(controller.hasCompletedFirstFetch) + + // Set up mock responses + MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId) + ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId) + + // Create test messages: + // Message 1: Matches early trigger "loginAction" + let message1Json = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "loginAction", + triggerId: "trigger1", + type: 2, // equal + value: "complete" + ) + let message1 = OSInAppMessageInternal.instance(withJson: message1Json)! + + // Message 2: Matches early trigger "sessionStart" + let message2Json = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "sessionStart", + triggerId: "trigger2", + type: 2, // equal + value: "true" + ) + let message2 = OSInAppMessageInternal.instance(withJson: message2Json)! + + // Message 3: Does not match any early trigger + let message3Json = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "otherAction", + triggerId: "trigger3", + type: 2, // equal + value: "something" + ) + let message3 = OSInAppMessageInternal.instance(withJson: message3Json)! + + // Mark messages 1 and 2 as previously displayed (so they're in redisplayedInAppMessages) + controller.redisplayedInAppMessages[message1.messageId] = message1 + controller.redisplayedInAppMessages[message2.messageId] = message2 + + // Set up IAM fetch response + let response = IAMTestHelpers.testFetchMessagesResponse(messages: [message1Json, message2Json, message3Json]) + client.setMockResponseForRequest( + request: "", + response: response) + + /* Execute */ + // Start the SDK and trigger first fetch + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2.0) + + /* Verify */ + // First fetch should have completed + XCTAssertTrue(controller.hasCompletedFirstFetch) + + // Early triggers should be cleared + XCTAssertEqual(controller.earlySessionTriggers.count, 0) + + // Messages should be received + XCTAssertEqual(controller.messages.count, 3) + + // Cast to properly typed array and find the messages + let messages = controller.messages as! [OSInAppMessageInternal] + let receivedMessage1 = messages.first { $0.messageId == message1.messageId } + let receivedMessage2 = messages.first { $0.messageId == message2.messageId } + let receivedMessage3 = messages.first { $0.messageId == message3.messageId } + + XCTAssertNotNil(receivedMessage1) + XCTAssertNotNil(receivedMessage2) + XCTAssertNotNil(receivedMessage3) + + // Messages matching early triggers and in redisplayedInAppMessages should have isTriggerChanged = true + XCTAssertTrue(receivedMessage1!.isTriggerChanged) + XCTAssertTrue(receivedMessage2!.isTriggerChanged) + + // Message 3 does not match early triggers (even though not in redisplayedInAppMessages anyway) + XCTAssertFalse(receivedMessage3!.isTriggerChanged) + } + + /** + Test that messages NOT in redisplayedInAppMessages don't get isTriggerChanged flag, even if they match early triggers. + + Scenario: + - Triggers are added before first fetch + - IAM fetch completes with messages that were NOT previously shown + - Messages match the early triggers + + Expected: + - Messages should NOT have isTriggerChanged = true (because they weren't in redisplayedInAppMessages) + */ + func testMessagesNotInRedisplayed_dontGetIsTriggerChangedFlag() throws { + /* Setup */ + let client = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(client) + OSMessagingController.start() + let controller = OSMessagingController.sharedInstance() + + // Add triggers before first fetch + controller.addTriggers(["newTrigger": "value"]) + + // Set up mock responses + MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId) + ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId) + + // Create a test message that matches the early trigger + let messageJson = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "newTrigger", + triggerId: "trigger1", + type: 2, // equal + value: "value" + ) + let message = OSInAppMessageInternal.instance(withJson: messageJson)! + + // Do NOT add this message to redisplayedInAppMessages + + // Set up IAM fetch response + let response = IAMTestHelpers.testFetchMessagesResponse(messages: [messageJson]) + client.setMockResponseForRequest( + request: "", + response: response) + + /* Execute */ + // Start the SDK and trigger first fetch + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1.0) + + /* Verify */ + XCTAssertTrue(controller.hasCompletedFirstFetch) + XCTAssertEqual(controller.messages.count, 1) + + let messages = controller.messages as! [OSInAppMessageInternal] + let receivedMessage = messages.first { $0.messageId == message.messageId } + XCTAssertNotNil(receivedMessage) + + // Message should NOT have isTriggerChanged because it's not in redisplayedInAppMessages + XCTAssertFalse(receivedMessage!.isTriggerChanged) + } + + /** + Test that earlySessionTriggers only applies to messages with shared trigger keys. + + Scenario: + - Multiple triggers added early + - Messages with different trigger combinations + + Expected: + - Only messages with matching trigger keys get isTriggerChanged + */ + func testIsTriggerChanged_onlyAppliedToMessagesWithMatchingTriggerKeys() throws { + /* Setup */ + let client = MockOneSignalClient() + OneSignalCoreImpl.setSharedClient(client) + OSMessagingController.start() + let controller = OSMessagingController.sharedInstance() + + // Add specific triggers before first fetch + controller.addTriggers(["action_a": "value1"]) + + // Set up mock responses + MockUserRequests.setDefaultCreateAnonUserResponses(with: client, onesignalId: testOneSignalId, subscriptionId: testSubscriptionId) + ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId) + + // Message 1: Uses trigger "action_a" (matches early triggers) + let message1Json = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "action_a", + triggerId: "trigger1", + type: 2, + value: "value1" + ) + let message1 = OSInAppMessageInternal.instance(withJson: message1Json)! + + // Message 2: Uses trigger "action_b" (does NOT match early triggers) + let message2Json = IAMTestHelpers.testMessageJsonWithTrigger( + kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, + property: "action_b", + triggerId: "trigger2", + type: 2, + value: "value2" + ) + let message2 = OSInAppMessageInternal.instance(withJson: message2Json)! + + // Mark both as previously displayed + controller.redisplayedInAppMessages[message1.messageId] = message1 + controller.redisplayedInAppMessages[message2.messageId] = message2 + + // Set up IAM fetch response + let response = IAMTestHelpers.testFetchMessagesResponse(messages: [message1Json, message2Json]) + client.setMockResponseForRequest( + request: "", + response: response) + + /* Execute */ + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1.0) + + /* Verify */ + let messages = controller.messages as! [OSInAppMessageInternal] + let receivedMessage1 = messages.first { $0.messageId == message1.messageId } + let receivedMessage2 = messages.first { $0.messageId == message2.messageId } + + XCTAssertNotNil(receivedMessage1) + XCTAssertNotNil(receivedMessage2) + + // Only message 1 should have isTriggerChanged (it has shared trigger "action_a") + XCTAssertTrue(receivedMessage1!.isTriggerChanged) + XCTAssertFalse(receivedMessage2!.isTriggerChanged) + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift index bfc661d7c..e4ec5585d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift @@ -52,7 +52,7 @@ final class OSMessagingControllerUserStateTests: XCTestCase { OneSignalUserMocks.reset() OSConsistencyManager.shared.reset() OSMessagingController.removeInstance() - + // Set up basic configuration OneSignalConfigManager.setAppId(testAppId) OneSignalLog.setLogLevel(.LL_VERBOSE) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h index c0f9be022..813316635 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OneSignalInAppMessagesTests-Bridging-Header.h @@ -8,7 +8,12 @@ // Expose private properties and methods for testing @interface OSMessagingController (Testing) +@property (nonatomic) BOOL hasCompletedFirstFetch; @property (strong, nonatomic, nonnull) NSMutableArray *messageDisplayQueue; +@property (strong, nonatomic, nonnull) NSMutableSet *earlySessionTriggers; +@property (strong, nonatomic, nonnull) NSMutableDictionary *redisplayedInAppMessages; +@property (strong, nonatomic, nonnull) NSMutableArray *messages; +@property (strong, nonatomic, nonnull) OSTriggerController *triggerController; + (void)start; + (void)removeInstance; - (void)presentInAppPreviewMessage:(OSInAppMessageInternal *)message;