Skip to content

Commit 1b0ec4d

Browse files
authored
fix: NotEqualTo trigger no longer matches non-existent properties (#1625)
* fix: NotEqualTo trigger no longer matches non-existent properties - Changes the behavior of trigger evaluation when a property doesn't exist. - Previously, a trigger with NotEqualTo operator would return true for non-existent properties (e.g., subscription_type != "premium" would match users without any subscription_type set). - Now, only the NotExists operator returns true for non-existent properties. NotEqualTo requires the property to exist with a different value. - This provides stricter trigger matching where NotEqualTo only matches users who have explicitly set a value that differs from the trigger value. * add testing for triggers change
1 parent f731bc5 commit 1b0ec4d

5 files changed

Lines changed: 132 additions & 8 deletions

File tree

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
3CA6CE0A28E4F19B00CA0585 /* OSUserRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CA6CE0928E4F19B00CA0585 /* OSUserRequest.swift */; };
150150
3CA8B8822BEC2FCB0010ADA1 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; };
151151
3CA8B8832BEC2FCB0010ADA1 /* XCTest.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3C7A39D42B7C18EE0082665E /* XCTest.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
152+
3CAA4BB72F0BAFBA00A16682 /* TriggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */; };
152153
3CBB6C262ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CBB6C252ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift */; };
153154
3CC063942B6D6B6B002BB07F /* OneSignalCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063932B6D6B6B002BB07F /* OneSignalCore.m */; };
154155
3CC063A22B6D7A8E002BB07F /* OneSignalCoreMocks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */; };
@@ -1352,6 +1353,7 @@
13521353
3C9AD6D02B228B9200BC1540 /* OSRequestRemoveAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestRemoveAlias.swift; sourceTree = "<group>"; };
13531354
3C9AD6D22B228BB000BC1540 /* OSRequestUpdateProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestUpdateProperties.swift; sourceTree = "<group>"; };
13541355
3CA6CE0928E4F19B00CA0585 /* OSUserRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserRequest.swift; sourceTree = "<group>"; };
1356+
3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerTests.swift; sourceTree = "<group>"; };
13551357
3CBB6C252ED59CCC000FEB02 /* ConsistencyManagerTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsistencyManagerTestHelpers.swift; sourceTree = "<group>"; };
13561358
3CC063932B6D6B6B002BB07F /* OneSignalCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalCore.m; sourceTree = "<group>"; };
13571359
3CC0639A2B6D7A8C002BB07F /* OneSignalCoreMocks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OneSignalCoreMocks.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -2155,6 +2157,7 @@
21552157
children = (
21562158
3C01519B2C2E29F90079E076 /* IAMRequestTests.m */,
21572159
3C7021E82ECF0CF4001768C6 /* IAMIntegrationTests.swift */,
2160+
3CAA4BB62F0BAFBA00A16682 /* TriggerTests.swift */,
21582161
3C7021E72ECF0CF3001768C6 /* OneSignalInAppMessagesTests-Bridging-Header.h */,
21592162
);
21602163
path = OneSignalInAppMessagesTests;
@@ -4295,6 +4298,7 @@
42954298
isa = PBXSourcesBuildPhase;
42964299
buildActionMask = 2147483647;
42974300
files = (
4301+
3CAA4BB72F0BAFBA00A16682 /* TriggerTests.swift in Sources */,
42984302
3C7021E92ECF0CF4001768C6 /* IAMIntegrationTests.swift in Sources */,
42994303
3C01519C2C2E29F90079E076 /* IAMRequestTests.m in Sources */,
43004304
);

iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSTriggerController.m

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,9 @@ - (BOOL)messageMatchesTriggers:(OSInAppMessageInternal *)message {
181181
- (BOOL)evaluateTrigger:(OSTrigger *)trigger forMessage:(OSInAppMessageInternal *)message {
182182
if (!self.triggers[trigger.property] && [trigger.kind isEqualToString:OS_DYNAMIC_TRIGGER_KIND_CUSTOM]) {
183183
// The value doesn't exist
184-
185-
// The condition for this trigger is true since the value doesn't exist
186-
// Either loop to the next condition, or return true if we are the last condition
187-
return trigger.operatorType == OSTriggerOperatorTypeNotExists ||
188-
(trigger.value && trigger.operatorType == OSTriggerOperatorTypeNotEqualTo);
184+
185+
// Only NotExists operator should return true for non-existent values
186+
return trigger.operatorType == OSTriggerOperatorTypeNotExists;
189187

190188
} else if (trigger.operatorType == OSTriggerOperatorTypeExists) {
191189
return true;

iOS_SDK/OneSignalSDK/OneSignalInAppMessagesMocks/IAMTestHelpers.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ public class IAMTestHelpers: NSObject {
7878

7979
/// Returns the JSON of an in-app message with trigger.
8080
@objc
81-
public static func testMessageJsonWithTrigger(property: String, triggerId: String, type: Int32, value: Any) -> [String: Any] {
81+
public static func testMessageJsonWithTrigger(kind: String, property: String, triggerId: String, type: Int32, value: Any) -> [String: Any] {
8282
var testMessage = self.testDefaultMessageJson()
8383

8484
testMessage["triggers"] = [
8585
[
8686
[
87-
"kind": property,
87+
"kind": kind,
8888
"property": property,
8989
"operator": OS_OPERATOR_TO_STRING(type),
9090
"value": value,

iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ final class IAMIntegrationTests: XCTestCase {
8888
MockUserRequests.setDefaultCreateAnonUserResponses(with: client)
8989

9090
// 3. Set up mock responses for fetching IAMs
91-
let message = IAMTestHelpers.testMessageJsonWithTrigger(property: "session_time", triggerId: "test_id1", type: 1, value: 10.0)
91+
let message = IAMTestHelpers.testMessageJsonWithTrigger(kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, property: "session_time", triggerId: "test_id1", type: 1, value: 10.0)
9292
let response = IAMTestHelpers.testFetchMessagesResponse(messages: [message])
9393
client.setMockResponseForRequest(
9494
request: "<OSRequestGetInAppMessages from apps/test-app-id/subscriptions/\(testPushSubId)/iams>",
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2025 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import XCTest
29+
import OneSignalInAppMessagesMocks
30+
31+
final class TriggerTests: XCTestCase {
32+
33+
override func setUpWithError() throws {
34+
// Put setup code here. This method is called before the invocation of each test method in the class.
35+
}
36+
37+
override func tearDownWithError() throws {
38+
// Put teardown code here. This method is called after the invocation of each test method in the class.
39+
}
40+
41+
/**
42+
Test that NotEqualTo trigger does NOT match non-existent properties.
43+
*/
44+
func testNotEqualToTrigger_doesNotMatchNonExistentProperty() throws {
45+
/* Setup */
46+
let triggerController = OSTriggerController()
47+
48+
// Create a message with NotEqualTo trigger
49+
let messageJson = IAMTestHelpers.testMessageJsonWithTrigger(
50+
kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
51+
property: "prop",
52+
triggerId: "test_trigger",
53+
type: 3, // OSTriggerOperatorTypeNotEqualTo
54+
value: "value"
55+
)
56+
let message = OSInAppMessageInternal.instance(withJson: messageJson)
57+
XCTAssertNotNil(message, "Message should be created successfully")
58+
59+
// Test 1: NotEqualTo should NOT match when property doesn't exist
60+
XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
61+
62+
// Test 2: NotEqualTo SHOULD match when property exists with different value
63+
triggerController.addTriggers(["prop": "other"])
64+
XCTAssertTrue(triggerController.messageMatchesTriggers(message!))
65+
66+
// Test 3: NotEqualTo should NOT match when property exists with same value
67+
triggerController.addTriggers(["prop": "value"])
68+
XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
69+
}
70+
71+
/**
72+
Test that NotExists trigger correctly matches non-existent properties.
73+
*/
74+
func testNotExistsTrigger_matchesNonExistentProperty() throws {
75+
/* Setup */
76+
let triggerController = OSTriggerController()
77+
78+
// Create a message with NotExists trigger
79+
let messageJson = IAMTestHelpers.testMessageJsonWithTrigger(
80+
kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
81+
property: "prop",
82+
triggerId: "test_trigger",
83+
type: 7, // OSTriggerOperatorTypeNotExists
84+
value: "value"
85+
)
86+
let message = OSInAppMessageInternal.instance(withJson: messageJson)
87+
XCTAssertNotNil(message, "Message should be created successfully")
88+
89+
// Test 1: NotExists SHOULD match when property doesn't exist
90+
XCTAssertTrue(triggerController.messageMatchesTriggers(message!))
91+
92+
// Test 2: NotExists should NOT match when property exists
93+
triggerController.addTriggers(["prop": "other"])
94+
XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
95+
}
96+
97+
/**
98+
Test that Exists trigger correctly matches existing properties.
99+
*/
100+
func testExistsTrigger_matchesExistingProperty() throws {
101+
/* Setup */
102+
let triggerController = OSTriggerController()
103+
104+
// Create a message with Exists trigger
105+
let messageJson = IAMTestHelpers.testMessageJsonWithTrigger(
106+
kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM,
107+
property: "prop",
108+
triggerId: "test_trigger",
109+
type: 6, // OSTriggerOperatorTypeExists
110+
value: "value"
111+
)
112+
let message = OSInAppMessageInternal.instance(withJson: messageJson)
113+
XCTAssertNotNil(message, "Message should be created successfully")
114+
115+
// Test 1: Exists should NOT match when property doesn't exist
116+
XCTAssertFalse(triggerController.messageMatchesTriggers(message!))
117+
118+
// Test 2: Exists SHOULD match when property exists
119+
triggerController.addTriggers(["prop": "other"])
120+
XCTAssertTrue(triggerController.messageMatchesTriggers(message!))
121+
}
122+
}

0 commit comments

Comments
 (0)