diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19e6d2662..cd5124329 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,19 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Migrated embedded message OOTB views to use Material Design buttons for better UI consistency
- Updated sample app Gradle configuration to use newer versions for better compatibility
+## [3.6.0-beta3]
+
+### Added
+- Added consent logging functionality for unknown user activation feature
+
+### Changed
+- Enhanced unknown user activation with improved criteria fetching and user ID generation logic
+
+### Fixed
+- Fixed unknown user activation to ensure criteria is fetched on foregrounding the app by default
+- Fixed unknown user ID generation to only occur once when multiple track calls are made
+- Fixed consent timestamp handling when consent is revoked
+
## [3.5.14]
### Fixed
- Fixed auth token refresh when app is in background, ensuring reliable token refresh in all app states.
@@ -63,6 +76,14 @@ IterableApi.initialize(context, apiKey, config);
- Added support for providing a list of placement ids to sync only certain placement ids.
- support for pre-release automation
+## [3.6.0-beta2]
+
+### Fixed
+- This release includes fixes for the Unknown user activation private beta:
+ - Criteria is now fetched on foregrounding the app by default. This feature can be turned off setting enableForegroundCriteriaFetch flag to false.
+ - Unknown user ids are only generated once when multiple track calls are made.
+- Unknown user activation is currently in private beta. If you'd like to learn more about it or discuss using it, talk to your Iterable customer success manager (who can also provide detailed documentation).
+
## [3.5.10]
### Added
@@ -109,6 +130,16 @@ IterableApi.initialize(context, apiKey, config);
- Addressed a text truncation issue in Embedded Message templates for applications targeting Android 14 and Android 15.
- Improved InboxActivity compatibility with edge-to-edge layouts, ensuring seamless handling of notches and display cutouts.
+## [3.6.0-beta1]
+
+#### Added
+- This release includes initial support for Unknown user activation, a feature that allows marketers to convert valuable visitors into customers. With this feature, the SDK can:
+ - Fetch unknown user profile creation criteria from your Iterable project, and then automatically create Iterable user profiles for unknown users who meet these criteria.
+ - Save information about a visitor's previous interactions with your application to their unknown user profile, after it's created.
+ - Display personalized messages for unknown users (in-app, push, and embedded messages).
+ - Merge unknown user profiles into an existing, known user profiles (when needed).
+- Unknown user activation is currently in private beta. If you'd like to learn more about it or discuss using it, talk to your Iterable customer success manager (who can also provide detailed documentation).
+
## [3.5.3]
#### Fixed
- Fixed an [issue](https://github.com/Iterable/react-native-sdk/issues/547) where the SDK would crash if the `IterableInAppMessage` object was null when consuming an in-app message.
diff --git a/app/.idea/workspace.xml b/app/.idea/workspace.xml
new file mode 100644
index 000000000..9c6424696
--- /dev/null
+++ b/app/.idea/workspace.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1728484381438
+
+
+ 1728484381438
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index b44c875ad..4348c851c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -50,6 +50,7 @@ dependencies {
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.fragment:fragment:1.8.5'
androidTestImplementation 'androidx.fragment:fragment-testing:1.8.5'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation project(':iterableapi')
implementation project(':iterableapi-ui')
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3b9b69d20..c77d25dba 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,9 @@
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme">
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/iterable/androidsdk/MainActivity.java b/app/src/main/java/com/iterable/androidsdk/MainActivity.java
index c5f0725b8..8e4ef2819 100644
--- a/app/src/main/java/com/iterable/androidsdk/MainActivity.java
+++ b/app/src/main/java/com/iterable/androidsdk/MainActivity.java
@@ -1,16 +1,28 @@
package com.iterable.androidsdk;
+import android.content.Intent;
import android.os.Bundle;
+
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
+
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
+
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
+import com.iterable.iterableapi.CommerceItem;
+import com.iterable.iterableapi.IterableApi;
import com.iterable.iterableapi.testapp.R;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
public class MainActivity extends AppCompatActivity {
@Override
@@ -19,6 +31,8 @@ protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
+ //Below api key is used to display merge user feature
+ IterableApi.initialize(this, "289895aa038648ee9e4ce60bd0a46e9c");
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@@ -28,6 +42,46 @@ public void onClick(View view) {
.setAction("Action", null).show();
}
});
+
+ findViewById(R.id.mainLayout).setOnLongClickListener(v -> {
+ Intent intent = new Intent(this, UnknownUserTrackingTestActivity.class);
+ startActivity(intent);
+ return true;
+ });
+
+ findViewById(R.id.btn_track_event).setOnClickListener(v -> IterableApi.getInstance().track("Browse Mocha"));
+
+ findViewById(R.id.btn_update_cart).setOnClickListener(v -> {
+ List items = new ArrayList<>();
+ items.add(new CommerceItem("123", "Mocha", 1, 1));
+ IterableApi.getInstance().updateCart(items);
+ });
+
+ findViewById(R.id.btn_buy_mocha).setOnClickListener(v -> {
+ List items = new ArrayList<>();
+ items.add(new CommerceItem("456", "Black Coffee", 2, 1));
+ IterableApi.getInstance().trackPurchase(4, items);
+ });
+
+ findViewById(R.id.btn_buy_coffee).setOnClickListener(v -> {
+ List items = new ArrayList<>();
+ items.add(new CommerceItem("456", "Black Coffee", 5, 1));
+ IterableApi.getInstance().trackPurchase(5, items);
+ });
+
+ findViewById(R.id.btn_set_user).setOnClickListener(v -> IterableApi.getInstance().setUserId("hani7"));
+
+ findViewById(R.id.btn_update_user).setOnClickListener(v -> {
+ try {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("firstName", "Hani");
+ IterableApi.getInstance().updateUser(jsonObject);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ findViewById(R.id.btn_logout).setOnClickListener(view -> IterableApi.getInstance().setUserId(null));
}
@Override
diff --git a/app/src/main/java/com/iterable/androidsdk/UnknownUserTrackingTestActivity.java b/app/src/main/java/com/iterable/androidsdk/UnknownUserTrackingTestActivity.java
new file mode 100644
index 000000000..fcaaa0e71
--- /dev/null
+++ b/app/src/main/java/com/iterable/androidsdk/UnknownUserTrackingTestActivity.java
@@ -0,0 +1,205 @@
+package com.iterable.androidsdk;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import com.iterable.iterableapi.AuthFailure;
+import com.iterable.iterableapi.CommerceItem;
+import com.iterable.iterableapi.IterableUnknownUserHandler;
+import com.iterable.iterableapi.IterableApi;
+import com.iterable.iterableapi.IterableAuthHandler;
+import com.iterable.iterableapi.IterableConfig;
+import com.iterable.iterableapi.IterableConstants;
+import com.iterable.iterableapi.testapp.R;
+import com.iterable.iterableapi.util.IterableJwtGenerator;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class UnknownUserTrackingTestActivity extends AppCompatActivity implements IterableUnknownUserHandler, IterableAuthHandler {
+
+ private CheckBox unknownUsageTrackedCheckBox;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_test);
+ unknownUsageTrackedCheckBox = findViewById(R.id.unknownUsageTracked_check_box);
+ IterableConfig iterableConfig = new IterableConfig.Builder().setEnableUnknownUserActivation(true).setUnknownUserHandler(this).setAuthHandler(this).build();
+
+ // clear data for testing
+ SharedPreferences sharedPref = getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, "");
+ editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ editor.putBoolean(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED, false);
+ editor.apply();
+
+ new Handler().postDelayed(() -> {
+ IterableApi.initialize(getBaseContext(), "bef41e4c1ab24b21bb3d65e47aa57a89", iterableConfig);
+ IterableApi.getInstance().setUserId(null);
+ IterableApi.getInstance().setEmail(null);
+ printAllSharedPreferencesData(this);
+ IterableApi.getInstance().setVisitorUsageTracked(unknownUsageTrackedCheckBox.isChecked());
+
+ }, 1000);
+
+ unknownUsageTrackedCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ IterableApi.getInstance().setVisitorUsageTracked(isChecked);
+ });
+
+ findViewById(R.id.updateCart).setOnClickListener(view -> {
+ EditText updateCart_edit = findViewById(R.id.updateCart_edit);
+ if(updateCart_edit == null) return;
+ Log.d("TEST_USER", String.valueOf(updateCart_edit.getText()));
+ try {
+ JSONArray cartJSOnItems = new JSONArray(String.valueOf(updateCart_edit.getText()));
+ List items = new ArrayList<>();
+ for(int i = 0; i < cartJSOnItems.length(); i++) {
+ final JSONObject item = cartJSOnItems.getJSONObject(i);
+ items.add(new CommerceItem(item.getString("id"), item.getString("name"), item.getDouble("price"), item.getInt("quantity")));
+ }
+ IterableApi.getInstance().updateCart(items);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ findViewById(R.id.trackPurchase).setOnClickListener(view -> {
+ EditText purchase_items = findViewById(R.id.trackPurchase_edit);
+ if(purchase_items == null) return;
+ Log.d("TEST_USER", String.valueOf(purchase_items.getText()));
+
+ int total;
+
+ try {
+ JSONObject jsonData = new JSONObject(String.valueOf(purchase_items.getText()));
+ total = (int) jsonData.get("total");
+ JSONArray items_array = jsonData.getJSONArray("items");
+ List items = new ArrayList<>();
+ for(int i = 0; i < items_array.length(); i++) {
+ final JSONObject item = items_array.getJSONObject(i);
+ items.add(new CommerceItem(item.getString("id"), item.getString("name"), item.getDouble("price"), item.getInt("quantity")));
+ }
+ IterableApi.getInstance().trackPurchase(total, items);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ findViewById(R.id.customEvent).setOnClickListener(view -> {
+ EditText customEvent_edit = findViewById(R.id.customEvent_edit);
+ if(customEvent_edit == null) return;
+ Log.d("TEST_USER", String.valueOf(customEvent_edit.getText()));
+
+ try {
+ JSONObject customEventItem = new JSONObject(String.valueOf(customEvent_edit.getText()));
+ JSONObject items = new JSONObject(customEventItem.get("dataFields").toString());
+ if(customEventItem.has("eventName")) {
+ items.put("eventName", customEventItem.getString("eventName"));
+ }
+ IterableApi.getInstance().track("customEvent", 0, 0, items);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ findViewById(R.id.updateUser).setOnClickListener(view -> {
+ EditText updateUser_edit = findViewById(R.id.updateUser_edit);
+ if(updateUser_edit == null) return;
+ Log.d("TEST_USER", String.valueOf(updateUser_edit.getText()));
+
+ try {
+ JSONObject updateUserItem = new JSONObject(String.valueOf(updateUser_edit.getText()));
+ IterableApi.getInstance().updateUser(updateUserItem);
+
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ findViewById(R.id.setUser).setOnClickListener(view -> {
+ EditText setUser_edit = findViewById(R.id.setUser_edit);
+ if(setUser_edit == null) return;;
+ IterableApi.getInstance().setUserId(String.valueOf(setUser_edit.getText()));
+ });
+ findViewById(R.id.setEmail).setOnClickListener(view -> {
+ EditText setEmail_edit = findViewById(R.id.setEmail_edit);
+ if(setEmail_edit == null) return;
+ IterableApi.getInstance().setEmail(String.valueOf(setEmail_edit.getText()));
+ });
+
+ findViewById(R.id.btn_logout).setOnClickListener(view -> {
+ IterableApi.getInstance().setUserId(null);
+ IterableApi.getInstance().setEmail(null);
+ });
+
+ }
+ public void printAllSharedPreferencesData(Context context) {
+ SharedPreferences sharedPref = context.getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ Map allEntries = sharedPref.getAll();
+
+ for (Map.Entry entry : allEntries.entrySet()) {
+ Log.d("SharedPref", entry.getKey() + ": " + entry.getValue().toString());
+ }
+ }
+
+ @Override
+ public void onUnknownUserCreated(String userId) {
+ Log.d("userId", userId);
+ }
+
+ @Override
+ public String onAuthTokenRequested() {
+ IterableApi instance = IterableApi.getInstance();
+ if (instance.getAuthToken() == null) {
+ final String secret = "1bb125ddcda2808f118c8b5e774d341c4b03fae68ebef0d140a22da1ec0295ad24d98981fd262c93bac98fa2e63a08142c0a36fe4322c09bea90f48c161780e0";
+ String userId;
+ String userEmail;
+ String jwtToken = null;
+ if (instance.getUserId() != null) {
+ userId = instance.getUserId(); // set as destination user Id
+ } else {
+ userId = null;
+ }
+ if (instance.getEmail() != null) {
+ userEmail = instance.getEmail(); // set as destination email Id
+ } else {
+ userEmail = null;
+ }
+ final Duration days7 = Duration.ofDays(7);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ jwtToken = IterableJwtGenerator.generateToken(secret, days7, userEmail, userId);
+ }
+ return jwtToken;
+ } else {
+ return instance.getAuthToken();
+ }
+ }
+
+ @Override
+ public void onTokenRegistrationSuccessful(String authToken) {
+ Log.d("Successful", authToken);
+ if (IterableApi.getInstance().getEmail() != null) {
+ Log.d("getEmail", IterableApi.getInstance().getEmail());
+ }
+ if (IterableApi.getInstance().getUserId() != null) {
+ Log.d("getUserId", IterableApi.getInstance().getUserId());
+ }
+ }
+
+ @Override
+ public void onAuthFailure(AuthFailure authFailure) {
+ Log.d("Failure", authFailure.failureReason.toString());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index f128d96bd..904a7bf1d 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -5,6 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
+ android:id="@+id/mainLayout"
tools:context="com.iterable.androidsdk.MainActivity">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml
index 5646874e1..a56897e97 100644
--- a/app/src/main/res/layout/content_main.xml
+++ b/app/src/main/res/layout/content_main.xml
@@ -1,5 +1,5 @@
-
+ tools:showIn="@layout/activity_main"
+ android:gravity="center"
+ android:orientation="vertical">
-
-
+ android:text="Browse Mocha" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/iterableapi/build.gradle b/iterableapi/build.gradle
index 31eed75b7..aadce0bad 100644
--- a/iterableapi/build.gradle
+++ b/iterableapi/build.gradle
@@ -61,7 +61,8 @@ dependencies {
api 'androidx.appcompat:appcompat:1.7.0'
api 'androidx.annotation:annotation:1.9.0'
api 'com.google.firebase:firebase-messaging:20.3.0'
- implementation "androidx.security:security-crypto-ktx:1.1.0-alpha06"
+ implementation 'com.google.code.gson:gson:2.10.1'
+ implementation "androidx.security:security-crypto:1.1.0-alpha06"
testImplementation 'junit:junit:4.13.2'
testImplementation 'androidx.test:runner:1.6.2'
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationComplexCriteriaCheckerTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationComplexCriteriaCheckerTest.java
new file mode 100644
index 000000000..7814c976c
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationComplexCriteriaCheckerTest.java
@@ -0,0 +1,655 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CombinationComplexCriteriaCheckerTest {
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ private final String complexCriteria1 = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"290\",\n" +
+ " \"name\": \"Complex Criteria Unit Test #1\",\n" +
+ " \"createdAt\": 1722532861551,\n" +
+ " \"updatedAt\": 1722532861551,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"A\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"B\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"C\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"saved_cars.color\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal-found.vaccinated\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"true\",\n" +
+ " \"fieldType\": \"boolean\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"total\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": \"100\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"reason\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"testing\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }";
+
+ @Test
+ public void complexCriteria1TestPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"reason\": \"new\",\n" +
+ " \"total\": 10\n" +
+ " },\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"vaccinated\": true\n" +
+ " },\n" +
+ " \"eventName\": \"animal-found\",\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"color\": \"black\"\n" +
+ " },\n" +
+ " \"eventName\": \"saved_cars\",\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"Adam\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria1, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void complexCriteria1TestFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"reason\": \"new\",\n" +
+ " \"total\": 10\n" +
+ " },\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"vaccinated\": true\n" +
+ " },\n" +
+ " \"eventName\": \"animal-found\",\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"color\": \"\"\n" +
+ " },\n" +
+ " \"eventName\": \"saved_cars\",\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"Adam\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria1, jsonArray);
+ assertNull(result);
+ }
+
+ private final String complexCriteria2 = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"291\",\n" +
+ " \"name\": \"Complex Criteria Unit Test #2\",\n" +
+ " \"createdAt\": 1722533473263,\n" +
+ " \"updatedAt\": 1722533473263,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"A\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"B\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"C\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"saved_cars.color\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal-found.vaccinated\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"true\",\n" +
+ " \"fieldType\": \"boolean\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"total\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"value\": \"100\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"reason\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"gift\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }";
+
+ @Test
+ public void complexCriteria2TestPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"reason\": \"new\",\n" +
+ " \"total\": 110\n" +
+ " },\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"vaccinated\": true\n" +
+ " },\n" +
+ " \"eventName\": \"animal-found\",\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"color\": \"black\"\n" +
+ " },\n" +
+ " \"eventName\": \"saved_cars\",\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"Xcode\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria2, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void complexCriteria2TestFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"reason\": \"gift\",\n" +
+ " \"total\": 10\n" +
+ " },\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"Alex\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria2, jsonArray);
+ assertNull(result);
+ }
+
+ private final String complexCriteria3 = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"292\",\n" +
+ " \"name\": \"Complex Criteria Unit Test #3\",\n" +
+ " \"createdAt\": 1722533789589,\n" +
+ " \"updatedAt\": 1722533838989,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"A\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"lastName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"A\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"C\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal-found.vaccinated\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"false\",\n" +
+ " \"fieldType\": \"boolean\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal-found.count\",\n" +
+ " \"comparatorType\": \"LessThan\",\n" +
+ " \"value\": \"5\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"total\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.quantity\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": \"34\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }";
+
+
+ @Test
+ public void complexCriteria3TestPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"purchase\",\n" +
+ " \"createdAt\": 1699246745093,\n" +
+ " \"items\": [{\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"coffee\",\n" +
+ " \"price\": \"100\",\n" +
+ " \"quantity\": \"2\"\n" +
+ " }]\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria3, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void complexCriteria3TestPass2() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"purchase\",\n" +
+ " \"createdAt\": 1699246745067,\n" +
+ " \"items\": [{\n" +
+ " \"id\": \"13\",\n" +
+ " \"name\": \"kittens\",\n" +
+ " \"price\": \"2\",\n" +
+ " \"quantity\": \"2\"\n" +
+ " }]\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria3, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void complexCriteria3TestFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"purchase\",\n" +
+ " \"createdAt\": 1699246745093,\n" +
+ " \"items\": [{\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"coffee\",\n" +
+ " \"price\": \"100\",\n" +
+ " \"quantity\": \"2\"\n" +
+ " }]\n" +
+ " },\n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"Alex\",\n" +
+ " \"lastName\": \"Aris\"\n" +
+ " }\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria3, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationLogicEventTypeCriteriaTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationLogicEventTypeCriteriaTest.java
new file mode 100644
index 000000000..16cdab180
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CombinationLogicEventTypeCriteriaTest.java
@@ -0,0 +1,1194 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CombinationLogicEventTypeCriteriaTest {
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ static final String mockDataCombinatorContactPropertyANDCustomEvent = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"310\",\n" +
+ " \"name\": \"Contact_Property_AND_Custom_Event\",\n" +
+ " \"createdAt\": 1723113771608,\n" +
+ " \"updatedAt\": 1723113771608,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"David\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"total\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+
+ @Test
+ public void testCompareDataContactPropertyANDCustomEventPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"David\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": \"10\"\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorContactPropertyANDCustomEvent, jsonArray);
+ assertTrue(result != null);
+ }
+ @Test
+ public void testCompareDataContactPropertyANDCustomEventFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"DavidJohn\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": \"11\"\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorContactPropertyANDCustomEvent, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorContactPropertyORCustomEvent = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"312\",\n" +
+ " \"name\": \"Contact_Property_OR_Custom_Event\",\n" +
+ " \"createdAt\": 1723115120517,\n" +
+ " \"updatedAt\": 1723115120517,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"David\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"total\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataContactPropertyORCustomEventPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"David\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": \"10\"\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorContactPropertyORCustomEvent, jsonArray);
+ assertTrue(result != null);
+ }
+ @Test
+ public void testCompareDataContactPropertyORCustomEventFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"DavidAs\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": \"101\"\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorContactPropertyORCustomEvent, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorContactPropertyNOTCustomEvent = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"312\",\n" +
+ " \"name\": \"Contact_Property_NOT_Custom_Event\",\n" +
+ " \"createdAt\": 1723115120517,\n" +
+ " \"updatedAt\": 1723115120517,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"David\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"total\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+
+ @Test
+ public void testCompareDataContactPropertyNOTCustomEventPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"Davidson\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": \"1\"\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorContactPropertyNOTCustomEvent, jsonArray);
+ assertTrue(result != null);
+ }
+ @Test
+ public void testCompareDataContactPropertyNOTCustomEventFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"David\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": \"10\"\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorContactPropertyNOTCustomEvent, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorUpdateCartANDContactProperty = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"315\",\n" +
+ " \"name\": \"UpdateCart_AND_ContactProperty\",\n" +
+ " \"createdAt\": 1723119153268,\n" +
+ " \"updatedAt\": 1723119153268,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"fried\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"David\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataUpdateCartANDContactPropertyPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"fried\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"David\"\n" +
+ " }\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorUpdateCartANDContactProperty, jsonArray);
+ assertTrue(result != null);
+ }
+ @Test
+ public void testCompareDataUpdateCartANDContactPropertyFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"boiled\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"David\"\n" +
+ " }\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorUpdateCartANDContactProperty, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorUpdateCartORContactProperty = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"315\",\n" +
+ " \"name\": \"UpdateCart_OR_ContactProperty\",\n" +
+ " \"createdAt\": 1723119153268,\n" +
+ " \"updatedAt\": 1723119153268,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"fried\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"David\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataUpdateCartORContactPropertyPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"fried\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"David\"\n" +
+ " }\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorUpdateCartORContactProperty, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataUpdateCartORContactPropertyFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"boiled\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"John\"\n" +
+ " }\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorUpdateCartORContactProperty, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorUpdateCartNOTContactProperty = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"315\",\n" +
+ " \"name\": \"UpdateCart_NOT_ContactProperty\",\n" +
+ " \"createdAt\": 1723119153268,\n" +
+ " \"updatedAt\": 1723119153268,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"fried\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"firstName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"David\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataUpdateCartNOTContactPropertyPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"boiled\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"DavidJohn\"\n" +
+ " }\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorUpdateCartNOTContactProperty, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataUpdateCartNOTContactPropertyFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"fried\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"eventType\": \"user\",\n" +
+ " \"dataFields\": {\n" +
+ " \"firstName\": \"David\"\n" +
+ " }\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorUpdateCartNOTContactProperty, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorPurchaseANDUpdateCart = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"316\",\n" +
+ " \"name\": \"Purchase_AND_UpdateCart\",\n" +
+ " \"createdAt\": 1723124161944,\n" +
+ " \"updatedAt\": 1723124205406,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"chicken\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"fried\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataPurchaseANDUpdateCartPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"fried\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"chicken\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorPurchaseANDUpdateCart, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataPurchaseANDUpdateCartFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"boiled\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"chicken\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorPurchaseANDUpdateCart, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorPurchaseORUpdateCart = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"316\",\n" +
+ " \"name\": \"Purchase_OR_UpdateCart\",\n" +
+ " \"createdAt\": 1723124161944,\n" +
+ " \"updatedAt\": 1723124205406,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"chicken\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"fried\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataPurchaseORUpdateCartPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"fried\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"chicken\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorPurchaseORUpdateCart, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataPurchaseORUpdateCartFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"boiled\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"beef\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorPurchaseORUpdateCart, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorPurchaseNOTUpdateCart = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"316\",\n" +
+ " \"name\": \"Purchase_NOT_UpdateCart\",\n" +
+ " \"createdAt\": 1723124161944,\n" +
+ " \"updatedAt\": 1723124205406,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"chicken\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"fried\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataPurchaseNOTUpdateCartPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"boiled\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"beef\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorPurchaseNOTUpdateCart, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataPurchaseNOTUpdateCartFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"fried\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " }, \n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"chicken\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorPurchaseNOTUpdateCart, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorCustomEventANDPurchase = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"320\",\n" +
+ " \"name\": \"CustomEvent_AND_Purchase\",\n" +
+ " \"createdAt\": 1723184939510,\n" +
+ " \"updatedAt\": 1723184939510,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"birthday\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"chicken\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataCustomEventANDPurchasePass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"dataFields\": {\n" +
+ " \"eventName\": \"birthday\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"chicken\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorCustomEventANDPurchase, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataCustomEventANDPurchaseFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"dataFields\": {\n" +
+ " \"eventName\": \"birthday\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"beef\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorCustomEventANDPurchase, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorCustomEventORPurchase = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"320\",\n" +
+ " \"name\": \"CustomEvent_OR_Purchase\",\n" +
+ " \"createdAt\": 1723184939510,\n" +
+ " \"updatedAt\": 1723184939510,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"birthday\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"chicken\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataCustomEventORPurchasePass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"dataFields\": {\n" +
+ " \"eventName\": \"birthday\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"beef\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorCustomEventORPurchase, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataCustomEventORPurchaseFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"dataFields\": {\n" +
+ " \"eventName\": \"anniversary\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"beef\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorCustomEventORPurchase, jsonArray);
+ assertFalse(result != null);
+ }
+
+ static final String mockDataCombinatorCustomEventNOTPurchase = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"320\",\n" +
+ " \"name\": \"CustomEvent_NOT_Purchase\",\n" +
+ " \"createdAt\": 1723184939510,\n" +
+ " \"updatedAt\": 1723184939510,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"birthday\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"chicken\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testCompareDataCustomEventNOTPurchasePass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"dataFields\": {\n" +
+ " \"eventName\": \"birthday1\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"chicken\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorCustomEventNOTPurchase, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataCustomEventNOTPurchaseFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"dataFields\": {\n" +
+ " \"eventName\": \"birthday\"\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"name\": \"chicken\"\n" +
+ " }\n" +
+ " ],\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCombinatorCustomEventNOTPurchase, jsonArray);
+ assertFalse(result != null);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/ComplexCriteriaCheckerTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/ComplexCriteriaCheckerTest.java
new file mode 100644
index 000000000..e4002078c
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/ComplexCriteriaCheckerTest.java
@@ -0,0 +1,834 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ComplexCriteriaCheckerTest {
+ private CriteriaCompletionChecker evaluator;
+
+ private String criteriaMinMax = "{"
+ + "\"count\": 1,"
+ + "\"criteriaSets\": ["
+ + " {"
+ + " \"criteriaId\": \"1\","
+ + " \"name\": \"Custom Event\","
+ + " \"createdAt\": 1716560453973,"
+ + " \"updatedAt\": 1716560453973,"
+ + " \"searchQuery\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"customEvent\","
+ + " \"minMatch\": 2,"
+ + " \"maxMatch\": 3,"
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.price\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"GreaterThanOrEqualTo\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 2,"
+ + " \"value\": \"50.0\""
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"dataType\": \"user\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"preferred_car_models\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Contains\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 4,"
+ + " \"value\": \"Honda\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + "]"
+ + "}";
+
+ private String complexCriteria1 = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"1\",\n" +
+ " \"name\": \"Custom Event\",\n" +
+ " \"createdAt\": 1716560453973,\n" +
+ " \"updatedAt\": 1716560453973,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"fieldType\": \"string\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"id\": 23,\n" +
+ " \"value\": \"button.clicked\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\": \"button-clicked.animal\",\n" +
+ " \"fieldType\": \"string\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"id\": 25,\n" +
+ " \"value\": \"giraffe\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.price\",\n" +
+ " \"fieldType\": \"double\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"id\": 28,\n" +
+ " \"value\": \"120\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.quantity\",\n" +
+ " \"fieldType\": \"long\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"id\": 29,\n" +
+ " \"valueLong\": 100,\n" +
+ " \"value\": \"100\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"fieldType\": \"string\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"id\": 31,\n" +
+ " \"value\": \"monitor\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\": \"shoppingCartItems.quantity\",\n" +
+ " \"fieldType\": \"long\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"id\": 32,\n" +
+ " \"valueLong\": 5,\n" +
+ " \"value\": \"5\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"field\": \"country\",\n" +
+ " \"fieldType\": \"string\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"id\": 34,\n" +
+ " \"value\": \"Japan\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\": \"preferred_car_models\",\n" +
+ " \"fieldType\": \"string\",\n" +
+ " \"comparatorType\": \"Contains\",\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"id\": 36,\n" +
+ " \"value\": \"Honda\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ private String complexCriteria2 = "{"
+ + "\"count\": 1,"
+ + "\"criteriaSets\": ["
+ + " {"
+ + " \"criteriaId\": \"1\","
+ + " \"name\": \"Custom Event\","
+ + " \"createdAt\": 1716560453973,"
+ + " \"updatedAt\": 1716560453973,"
+ + " \"searchQuery\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"combinator\": \"Or\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"customEvent\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"eventName\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 2,"
+ + " \"value\": \"button-clicked\""
+ + " },"
+ + " {"
+ + " \"field\": \"button-clicked.lastPageViewed\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 4,"
+ + " \"value\": \"welcome page\""
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"dataType\": \"customEvent\","
+ + " \"minMatch\": 2,"
+ + " \"maxMatch\": 3,"
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.price\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"GreaterThanOrEqualTo\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 6,"
+ + " \"value\": \"85\""
+ + " },"
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.quantity\","
+ + " \"fieldType\": \"long\","
+ + " \"comparatorType\": \"GreaterThanOrEqualTo\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 7,"
+ + " \"valueLong\": 50,"
+ + " \"value\": \"50\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " },"
+ + " {"
+ + " \"combinator\": \"Or\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"purchase\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"shoppingCartItems.name\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 16,"
+ + " \"isFiltering\": false,"
+ + " \"value\": \"coffee\""
+ + " },"
+ + " {"
+ + " \"field\": \"shoppingCartItems.quantity\","
+ + " \"fieldType\": \"long\","
+ + " \"comparatorType\": \"GreaterThanOrEqualTo\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 17,"
+ + " \"valueLong\": 2,"
+ + " \"value\": \"2\""
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"dataType\": \"user\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"country\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 19,"
+ + " \"value\": \"USA\""
+ + " },"
+ + " {"
+ + " \"field\": \"preferred_car_models\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Contains\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 21,"
+ + " \"value\": \"Subaru\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + "]"
+ + "}";
+
+ private String complexCriteria3 = "{"
+ + "\"count\": 1,"
+ + "\"criteriaSets\": ["
+ + " {"
+ + " \"criteriaId\": \"1\","
+ + " \"name\": \"Custom Event\","
+ + " \"createdAt\": 1716560453973,"
+ + " \"updatedAt\": 1716560453973,"
+ + " \"searchQuery\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"customEvent\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"eventName\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 2,"
+ + " \"value\": \"button-clicked\""
+ + " },"
+ + " {"
+ + " \"field\": \"button-clicked.lastPageViewed\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 4,"
+ + " \"value\": \"welcome page\""
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"dataType\": \"customEvent\","
+ + " \"minMatch\": 2,"
+ + " \"maxMatch\": 3,"
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.price\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"GreaterThanOrEqualTo\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 6,"
+ + " \"value\": \"85\""
+ + " },"
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.quantity\","
+ + " \"fieldType\": \"long\","
+ + " \"comparatorType\": \"GreaterThanOrEqualTo\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 7,"
+ + " \"valueLong\": 50,"
+ + " \"value\": \"50\""
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"dataType\": \"purchase\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"shoppingCartItems.name\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 9,"
+ + " \"value\": \"coffee\""
+ + " },"
+ + " {"
+ + " \"field\": \"shoppingCartItems.quantity\","
+ + " \"fieldType\": \"long\","
+ + " \"comparatorType\": \"GreaterThanOrEqualTo\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 10,"
+ + " \"valueLong\": 2,"
+ + " \"value\": \"2\""
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"dataType\": \"user\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"country\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Equals\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 12,"
+ + " \"value\": \"USA\""
+ + " },"
+ + " {"
+ + " \"field\": \"preferred_car_models\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"Contains\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 14,"
+ + " \"value\": \"Subaru\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + "]"
+ + "}";
+
+ private String complexCriteria4 = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"1\",\n" +
+ " \"name\": \"Custom Event\",\n" +
+ " \"createdAt\": 1716560453973,\n" +
+ " \"updatedAt\": 1716560453973,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Not\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"fieldType\": \"string\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"id\": 1,\n" +
+ " \"value\": \"sneakers\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\": \"shoppingCartItems.quantity\",\n" +
+ " \"fieldType\": \"long\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"id\": 2,\n" +
+ " \"valueLong\": 3,\n" +
+ " \"value\": \"3\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"fieldType\": \"string\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"id\": 4,\n" +
+ " \"value\": \"slippers\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\": \"shoppingCartItems.quantity\",\n" +
+ " \"fieldType\": \"long\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"id\": 5,\n" +
+ " \"valueLong\": 3,\n" +
+ " \"value\": \"3\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testMinMatchWithCriteriaMinMaxData() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":50,\\\"quantity\\\":40}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":50,\\\"quantity\\\":40}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Honda\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(criteriaMinMax, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testMinMatchWithCriteriaMinMaxDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":50,\\\"quantity\\\":40}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Honda\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(criteriaMinMax, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria1() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"monitor\\\",\\\"price\\\":50,\\\"quantity\\\":10}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Honda\",\n" +
+ " \"country\": \"Japan\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria1, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria1Fail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"monitor\\\",\\\"price\\\":50,\\\"quantity\\\":10}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Honda\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria1, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria2() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":90,\\\"quantity\\\":50}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":90,\\\"quantity\\\":50}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Subaru\",\n" +
+ " \"country\": \"USA\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria2, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria2Fail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":90,\\\"quantity\\\":50}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Subaru\",\n" +
+ " \"country\": \"USA\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria2, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria3() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":90,\\\"quantity\\\":50}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":90,\\\"quantity\\\":50}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Subaru\",\n" +
+ " \"country\": \"USA\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"eventName\": \"button-clicked\",\n" +
+ " \"dataFields\": {\n" +
+ " \"lastPageViewed\": \"welcome page\"\n" +
+ " },\n" +
+ " \"total\": 3,\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"coffee\",\n" +
+ " \"price\": 10,\n" +
+ " \"quantity\": 5\n" +
+ " }\n" +
+ " ],\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 2,\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"eventName\": \"button-clicked\",\n" +
+ " \"dataFields\": {\n" +
+ " \"lastPageViewed\": \"welcome page\"\n" +
+ " },\n" +
+ " \"total\": 3,\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria3, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria3Fail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":90,\\\"quantity\\\":50}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"items\": \"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":90,\\\"quantity\\\":50}]\",\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 50,\n" +
+ " \"eventType\": \"cartUpdate\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"preferred_car_models\": \"Subaru\",\n" +
+ " \"country\": \"USA\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"eventName\": \"button-clicked\",\n" +
+ " \"dataFields\": {\n" +
+ " \"lastPageViewed\": \"welcome page\"\n" +
+ " },\n" +
+ " \"total\": 3,\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria3, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria4() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"sneakers\",\n" +
+ " \"price\": 10,\n" +
+ " \"quantity\": 5\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"slippers\",\n" +
+ " \"price\": 10,\n" +
+ " \"quantity\": 5\n" +
+ " }\n" +
+ " ],\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 2,\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria4, jsonArray);
+ System.out.println("TEST_USER: " + String.valueOf(result));
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testComplexCriteria4Fail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"sneakers\",\n" +
+ " \"price\": 10,\n" +
+ " \"quantity\": 3\n" +
+ " },\n" +
+ " {\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"slippers\",\n" +
+ " \"price\": 10,\n" +
+ " \"quantity\": 5\n" +
+ " }\n" +
+ " ],\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 2,\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(complexCriteria4, jsonArray);
+ System.out.println("TEST_USER: " + String.valueOf(result));
+ assertFalse(result != null);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java
new file mode 100644
index 000000000..fd8ead79f
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java
@@ -0,0 +1,539 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CriteriaCompletionCheckerTest {
+
+ private CriteriaCompletionChecker evaluator;
+
+ private final String mockDataWithAnd = "{\n" +
+ " \"count\":2,\n" +
+ " \"criteriaSets\":[\n" +
+ " {\n" +
+ " \"criteriaId\":12345,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.price\",\n" +
+ " \"fieldType\":\"double\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":2,\n" +
+ " \"value\":\"4.67\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.quantity\",\n" +
+ " \"fieldType\":\"long\",\n" +
+ " \"comparatorType\":\"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":3,\n" +
+ " \"valueLong\":2,\n" +
+ " \"value\":\"2\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"criteriaId\":5678,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"itblInternal.emailDomain\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"id\":6,\n" +
+ " \"value\":\"gmail.com\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"eventName\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":9,\n" +
+ " \"value\":\"processing_cancelled\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"createdAt\",\n" +
+ " \"fieldType\":\"date\",\n" +
+ " \"comparatorType\":\"GreaterThan\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":10,\n" +
+ " \"dateRange\":{\n" +
+ " \n" +
+ " },\n" +
+ " \"isRelativeDate\":false,\n" +
+ " \"value\":\"1688194800000\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ private final String mockDataWithOr = "{\n" +
+ " \"count\":2,\n" +
+ " \"criteriaSets\":[\n" +
+ " {\n" +
+ " \"criteriaId\":12345,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.price\",\n" +
+ " \"fieldType\":\"double\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":2,\n" +
+ " \"value\":\"4.67\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.quantity\",\n" +
+ " \"fieldType\":\"long\",\n" +
+ " \"comparatorType\":\"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":3,\n" +
+ " \"valueLong\":2,\n" +
+ " \"value\":\"2\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"criteriaId\":5678,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"itblInternal.emailDomain\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"id\":6,\n" +
+ " \"value\":\"gmail.com\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"eventName\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":9,\n" +
+ " \"value\":\"processing_cancelled\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"createdAt\",\n" +
+ " \"fieldType\":\"date\",\n" +
+ " \"comparatorType\":\"GreaterThan\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":10,\n" +
+ " \"dateRange\":{\n" +
+ " \n" +
+ " },\n" +
+ " \"isRelativeDate\":false,\n" +
+ " \"value\":\"1688194800000\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ private String mockCriteria = "{\n" +
+ " \"count\": 4,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"43\",\n" +
+ " \"name\": \"ContactProperty\",\n" +
+ " \"createdAt\": 1716560453973,\n" +
+ " \"updatedAt\": 1716560453973,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"UK\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"preferred_car_models\",\n" +
+ " \"comparatorType\": \"Contains\",\n" +
+ " \"value\": \"Mazda\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"criteriaId\": \"42\",\n" +
+ " \"name\": \"purchase\",\n" +
+ " \"createdAt\": 1716560403912,\n" +
+ " \"updatedAt\": 1716560403912,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.name\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"keyboard\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.quantity\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"value\": \"3\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"shoppingCartItems.price\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"criteriaId\": \"41\",\n" +
+ " \"name\": \"updateCart\",\n" +
+ " \"createdAt\": 1716560369947,\n" +
+ " \"updatedAt\": 1716560369947,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"updateCart\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.price\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " \"minMatch\": 3,\n" +
+ " \"maxMatch\": 2\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"updateCart\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"updateCart.updatedShoppingCartItems.quantity\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"value\": \"50\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"criteriaId\": \"40\",\n" +
+ " \"name\": \"Customevent\",\n" +
+ " \"createdAt\": 1716560323583,\n" +
+ " \"updatedAt\": 1716560323583,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"button-clicked\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"button-clicked.lastPageViewed\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"signup page\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testCompareDataWithANDCombinatorFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":3.5,\\\"quantity\\\":6}]\",\"createdAt\":1700071052507,\"total\":4.67,\"dataType\":\"purchase\"}]");
+ boolean result = evaluator.getMatchedCriteria(mockDataWithAnd, jsonArray) != null;
+ assertFalse(result);
+ }
+
+ @Test
+ public void testCompareDataWithANDCombinator() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":4.67,\\\"quantity\\\":3}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"purchase\"}]");
+ boolean result = evaluator.getMatchedCriteria(mockDataWithAnd, jsonArray) != null;
+ assertTrue(result);
+ }
+
+ @Test
+ public void testCompareDataWithORCombinator() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":4.67,\\\"quantity\\\":3}]\",\"createdAt\":1700071052507,\"total\":2,\"eventType\":\"purchase\"}]");
+ boolean result = evaluator.getMatchedCriteria(mockDataWithOr, jsonArray) != null;
+ assertTrue(result);
+ }
+
+ @Test
+ public void testCompareDataWithORCombinatorFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":3.5,\\\"quantity\\\":1}]\",\"createdAt\":1700071052507,\"total\":4.67,\"dataType\":\"purchase\"}]");
+ boolean result = evaluator.getMatchedCriteria(mockDataWithOr, jsonArray) != null;
+ assertFalse(result);
+ }
+
+ @Test
+ public void testUserWithMockData() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"dataFields\":{\"country\":\"UK\"},\"eventType\":\"user\"}]");
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testUserWithMockDataFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"dataFields\":{\"country\":\"US\"},\"eventType\":\"user\"}]");
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testPurchaseWithMockData() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"keyboard\\\",\\\"price\\\":10,\\\"quantity\\\":2}]\",\"createdAt\":1700071052507,\"total\":2,\"eventType\":\"purchase\"}]");
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testPurchaseWithMockDataFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"guitar\\\",\\\"price\\\":15,\\\"quantity\\\":2}]\",\"createdAt\":1700071052507,\"total\":2,\"eventType\":\"purchase\"}]");
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testUpdateCartWithMockData() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":9,\\\"quantity\\\":52}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"cartUpdate\"}]");
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testUpdateCartWithMockDataFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":9,\\\"quantity\\\":40}]\",\"createdAt\":1700071052507,\"total\":9,\"eventType\":\"cartUpdate\"}]");
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testCustomEventWithMockData() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"eventName\":\"button-clicked\",\"dataFields\":{\"lastPageViewed\":\"signup page\"},\"createdAt\":1700071052507,\"eventType\":\"customEvent\"}]");
+
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCustomEventWithMockDataFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"eventName\":\"button-clicked\",\"dataFields\":{\"lastPageViewed\":\"login page\"},\"createdAt\":1700071052507,\"eventType\":\"customEvent\"}]");
+
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testSingleItemMatchWithMockDataFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"piano\\\",\\\"price\\\":10,\\\"quantity\\\":2},{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"piano\\\",\\\"price\\\":5,\\\"quantity\\\":3}]\",\"createdAt\":1700071052507,\"total\":2,\"eventType\":\"purchase\"}]");
+
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testMinMatchWithMockData() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":10,\\\"quantity\\\":40}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"cartUpdate\"},{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":10,\\\"quantity\\\":40}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"cartUpdate\"},{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":10,\\\"quantity\\\":40}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"cartUpdate\"}]");
+
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testMinMatchWithMockDataFail() throws Exception {
+ JSONArray jsonArray = new JSONArray("[{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":10,\\\"quantity\\\":40}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"cartUpdate\"},{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":10,\\\"quantity\\\":40}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"cartUpdate\"},{\"items\":\"[{\\\"id\\\":\\\"12\\\",\\\"name\\\":\\\"Mocha\\\",\\\"price\\\":15,\\\"quantity\\\":40}]\",\"createdAt\":1700071052507,\"total\":4.67,\"eventType\":\"cartUpdate\"}]");
+
+ String result = evaluator.getMatchedCriteria(mockCriteria, jsonArray);
+ assertFalse(result != null);
+ }
+
+}
\ No newline at end of file
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionComparatorTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionComparatorTest.java
new file mode 100644
index 000000000..346d8bf97
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionComparatorTest.java
@@ -0,0 +1,415 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CriteriaCompletionComparatorTest {
+ private CriteriaCompletionChecker evaluator;
+ private String isSetMockData = "{"
+ + "\"count\": 4,"
+ + "\"criteriaSets\": ["
+ + " {"
+ + " \"criteriaId\": \"1\","
+ + " \"name\": \"CustomEvent\","
+ + " \"createdAt\": 1716560453973,"
+ + " \"updatedAt\": 1716560453973,"
+ + " \"searchQuery\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"customEvent\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"button-clicked\","
+ + " \"fieldType\": \"object\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 2,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"button-clicked.animal\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 4,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"button-clicked.clickCount\","
+ + " \"fieldType\": \"long\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 5,"
+ + " \"valueLong\": null,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"total\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 9,"
+ + " \"value\": \"\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"criteriaId\": \"2\","
+ + " \"name\": \"UpdateCart\","
+ + " \"createdAt\": 1716560453973,"
+ + " \"updatedAt\": 1716560453973,"
+ + " \"searchQuery\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"customEvent\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"updateCart\","
+ + " \"fieldType\": \"object\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 9,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.name\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 13,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.price\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 15,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"updateCart.updatedShoppingCartItems.quantity\","
+ + " \"fieldType\": \"long\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"customEvent\","
+ + " \"id\": 16,"
+ + " \"valueLong\": null,"
+ + " \"value\": \"\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"criteriaId\": \"3\","
+ + " \"name\": \"Purchase\","
+ + " \"createdAt\": 1716560453973,"
+ + " \"updatedAt\": 1716560453973,"
+ + " \"searchQuery\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"purchase\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"shoppingCartItems\","
+ + " \"fieldType\": \"object\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 1,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"shoppingCartItems.price\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 3,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"shoppingCartItems.name\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 5,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"total\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"purchase\","
+ + " \"id\": 7,"
+ + " \"value\": \"\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + " },"
+ + " {"
+ + " \"criteriaId\": \"4\","
+ + " \"name\": \"User\","
+ + " \"createdAt\": 1716560453973,"
+ + " \"updatedAt\": 1716560453973,"
+ + " \"searchQuery\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"dataType\": \"user\","
+ + " \"searchCombo\": {"
+ + " \"combinator\": \"And\","
+ + " \"searchQueries\": ["
+ + " {"
+ + " \"field\": \"country\","
+ + " \"fieldType\": \"string\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 25,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"eventTimeStamp\","
+ + " \"fieldType\": \"long\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 26,"
+ + " \"valueLong\": null,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"phoneNumberDetails\","
+ + " \"fieldType\": \"object\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 28,"
+ + " \"value\": \"\""
+ + " },"
+ + " {"
+ + " \"field\": \"shoppingCartItems.price\","
+ + " \"fieldType\": \"double\","
+ + " \"comparatorType\": \"IsSet\","
+ + " \"dataType\": \"user\","
+ + " \"id\": 30,"
+ + " \"value\": \"\""
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + " ]"
+ + " }"
+ + " ]"
+ + " }"
+ + " }"
+ + "]"
+ + "}";
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testUserWithIsSetMockData() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": \"UK\",\n" +
+ " \"eventTimeStamp\": 10,\n" +
+ " \"phoneNumberDetails\": \"99999999\",\n" +
+ " \"shoppingCartItems.price\": 50.5\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testUserWithIsSetMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 10,\n" +
+ " \"phoneNumberDetails\": \"99999999\",\n" +
+ " \"shoppingCartItems.price\": 50.5\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testPurchaseWithIsSetMockData() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"keyboard\",\n" +
+ " \"price\": 10,\n" +
+ " \"quantity\": 2\n" +
+ " }\n" +
+ " ],\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 2,\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testPurchaseWithIsSetMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"items\": [\n" +
+ " {\n" +
+ " \"id\": \"12\",\n" +
+ " \"name\": \"keyboard\"," +
+ " \"quantity\": 5\n" +
+ " }\n" +
+ " ],\n" +
+ " \"createdAt\": 1700071052507,\n" +
+ " \"total\": 2,\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testUpdateCartWithIsSetMockData() throws Exception {
+ String jsonString = "["
+ + " {"
+ + " \"items\": ["
+ + " {"
+ + " \"id\": \"12\","
+ + " \"name\": \"Mocha\","
+ + " \"price\": 9,"
+ + " \"quantity\": 52"
+ + " }"
+ + " ],"
+ + " \"createdAt\": 1700071052507,"
+ + " \"eventType\": \"cartUpdate\""
+ + " }"
+ + "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testUpdateCartWithIsSetMockDataFail() throws Exception {
+ String jsonString = "["
+ + " {"
+ + " \"items\": ["
+ + " {"
+ + " \"id\": \"12\","
+ + " \"name\": \"Mocha\","
+ + " \"quantity\": 5"
+ + " }"
+ + " ],"
+ + " \"createdAt\": 1700071052507,"
+ + " \"eventType\": \"cartUpdate\""
+ + " }"
+ + "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testCustomEventWithIsSetMockData() throws Exception {
+ String jsonString = "["
+ + " {"
+ + " \"eventName\": \"button-clicked\","
+ + " \"dataFields\": {"
+ + " \"animal\": \"test page\","
+ + " \"clickCount\": \"2\""
+ + " },"
+ + " \"total\": 3,"
+ + " \"createdAt\": 1700071052507,"
+ + " \"eventType\": \"customEvent\""
+ + " }"
+ + "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCustomEventWithIsSetMockDataFail() throws Exception {
+ String jsonString = "["
+ + " {"
+ + " \"eventName\": \"button-clicked\","
+ + " \"dataFields\": {"
+ + " \"animal\": \"\""
+ + " },"
+ + " \"total\": 3,"
+ + " \"createdAt\": 1700071052507,"
+ + " \"eventType\": \"customEvent\""
+ + " }"
+ + "]";
+
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(isSetMockData, jsonArray);
+ assertFalse(result != null);
+ }
+
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorArrayInputCriteriaTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorArrayInputCriteriaTest.java
new file mode 100644
index 000000000..9b0bc6fed
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorArrayInputCriteriaTest.java
@@ -0,0 +1,788 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DataTypeComparatorArrayInputCriteriaTest {
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ static final String mockDataArrayDataTypeWithEquals = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_Equals\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"milestoneYears\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"1997\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"score\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": 11.5,\n" +
+ " \"fieldType\": \"double\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"timestamp\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": 1722500215276,\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testEqualsArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1996, 1997, 2002, 2020, 2024],\n" +
+ " \"score\": [10.5, 11.5, 12.5, 13.5, 14.5],\n" +
+ " \"timestamp\": [1722497422151, 1722500235276, 1722500215276, 1722500225276,\n" +
+ " 1722500245276]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithEquals, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testEqualsArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1996, 1998, 2002, 2020, 2024],\n" +
+ " \"score\": [10.5, 11.5, 12.5, 13.5, 14.5],\n" +
+ " \"timestamp\": [1722497422151, 1722500235276, 1722500215276, 1722500225276,\n" +
+ " 1722500245276]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithEquals, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataArrayDataTypeWithDoesNotEqual = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_DoesNotEqual\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"milestoneYears\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"1997\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"score\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": 11.5,\n" +
+ " \"fieldType\": \"double\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"timestamp\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": 1722500215276,\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testDoesNotEqualArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1996, 1998, 2002, 2020, 2024],\n" +
+ " \"score\": [10.5, 8.5, 12.5, 13.5, 14.5],\n" +
+ " \"timestamp\": [1722497422151, 1722500235276, 1722500215275, 1722500225276,\n" +
+ " 1722500245276]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithDoesNotEqual, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testDoesNotEqualArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1996, 1998, 2002, 2020, 2024],\n" +
+ " \"score\": [10.5, 11.5, 12.5, 13.5, 14.5],\n" +
+ " \"timestamp\": [1722497422151, 1722500235276, 1722500215276, 1722500225276,\n" +
+ " 1722500245276]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithDoesNotEqual, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataArrayDataTypeWithGreaterThan = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_GreaterThan\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"milestoneYears\",\n" +
+ " \"comparatorType\": \"GreaterThan\",\n" +
+ " \"value\": 1997,\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testGreaterThanArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1996, 1998, 2002, 2020, 2024]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithGreaterThan, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testGreaterThanArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1990, 1992, 1994, 1997]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithGreaterThan, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataArrayDataTypeWithGreaterThanOrEqualTo = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_GreaterThanOrEqualTo\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"milestoneYears\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"value\": 1997,\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testGreaterThanOrEqualArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1997, 1998, 2002, 2020, 2024]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithGreaterThanOrEqualTo, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testGreaterThanOrEqualArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1990, 1992, 1994, 1996]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithGreaterThanOrEqualTo, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataArrayDataTypeWithLessThan = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_LessThan\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"milestoneYears\",\n" +
+ " \"comparatorType\": \"LessThan\",\n" +
+ " \"value\": 1997,\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testLessThanArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1990, 1992, 1994, 1996, 1998]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithLessThan, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testLessThanArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1997, 1999, 2002, 2004]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithLessThan, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataArrayDataTypeWithLessThanOrEqualTo = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_LessThanOrEqualTo\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"milestoneYears\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": 1997,\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testLessThanOrEqualsToArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1990, 1992, 1994, 1996, 1998]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithLessThanOrEqualTo, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testLessThanOrEqualsToArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"milestoneYears\": [1998, 1999, 2002, 2004]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithLessThanOrEqualTo, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataArrayDataTypeWithContains = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_Contains\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"Contains\",\n" +
+ " \"value\": \"UK\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testContainArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": [\"US\", \"UK\", \"China\", \"Europe\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithContains, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testContainArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": [\"US\", \"Canada\", \"China\", \"Europe\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithContains, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataArrayDataTypeWithMatchesRegex = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_MatchesRegex\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"MatchesRegex\",\n" +
+ " \"value\": \"^T.*iwa.*n$\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testMatchesRegexArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": [\"US\", \"Taiwan\", \"China\", \"Europe\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithMatchesRegex, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testMatchesRegexArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": [\"US\", \"Thailand\", \"China\", \"Europe\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithMatchesRegex, jsonArray);
+ assertNull(result);
+ }
+
+
+ static final String mockDataArrayDataTypeWithStartsWith = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_StartsWith\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"T\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testStartsWithArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": [\"US\", \"Taiwan\", \"China\", \"Europe\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithStartsWith, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testStartsWithArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": [\"US\", \"Canada\", \"China\", \"Europe\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayDataTypeWithStartsWith, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataStringArrayMixCriteriaArea = " {\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"382\",\n" +
+ " \"name\": \"comparison_for_Array_data_types_or\",\n" +
+ " \"createdAt\": 1724315593795,\n" +
+ " \"updatedAt\": 1724315593795,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"Or\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"milestoneYears\",\n" +
+ " \"comparatorType\": \"GreaterThan\",\n" +
+ " \"value\": \"1997\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"button-clicked.animal\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"giraffe\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"purchase\",\n" +
+ " \"field\": \"total\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": \"200\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n";
+
+ @Test
+ public void testMixCriteriaAreaArrayMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": { \n" +
+ " \"createdAt\": 1699246745093,\n" +
+ " \"milestoneYears\": [1998, 1999, 2002, 2004]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"button-clicked.animal\": [\"cow\", \"horse\"]\n" +
+ " }, \n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": [199.99, 210.0, 220.20, 250.10]\n" +
+ " },\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataStringArrayMixCriteriaArea, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testMixCriteriaAreaArrayMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": { \n" +
+ " \"createdAt\": 1699246745093,\n" +
+ " \"milestoneYears\": [1990, 1992, 1996, 1997]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"button-clicked.animal\": [\"cow\", \"horse\", \"giraffe\"]\n" +
+ " }, \n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"total\": [210.0, 220.20, 250.10]\n" +
+ " },\n" +
+ " \"eventType\": \"purchase\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataStringArrayMixCriteriaArea, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorSearchQueryCriteriaTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorSearchQueryCriteriaTest.java
new file mode 100644
index 000000000..a5578472f
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DataTypeComparatorSearchQueryCriteriaTest.java
@@ -0,0 +1,810 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DataTypeComparatorSearchQueryCriteriaTest {
+
+ static final String mockDataEquals = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"285\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_Equals\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"3\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"19.99\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"likes_boba\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"true\",\n" +
+ " \"fieldType\": \"boolean\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"China\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataDoesNotEqual = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"286\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_DoesNotEqual\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"101\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"101\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"likes_boba\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"false\",\n" +
+ " \"fieldType\": \"boolean\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"China\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataGreaterThan = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"287\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_GreaterThan\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"GreaterThan\",\n" +
+ " \"value\": \"7\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"GreaterThan\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataGreaterThanOrEqualTo = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"288\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_GreaterThanOrEqualTo\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"value\": \"12\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"GreaterThanOrEqualTo\",\n" +
+ " \"value\": \"20\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataLessThan = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"289\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_LessThan\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"LessThan\",\n" +
+ " \"value\": \"6\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"LessThan\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataLessThanOrEqualTo = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"290\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_LessThanOrEqualTo\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": \"56\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"LessThanOrEqualTo\",\n" +
+ " \"value\": \"60\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+
+ static final String mockDataIsSet = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"291\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_IsSet\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"saved_cars\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataContains = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"291\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_IsSet\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"Contains\",\n" +
+ " \"value\": \"Taiwan\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataMatchesRegex = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"291\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_IsSet\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"MatchesRegex\",\n" +
+ " \"value\": \"^T.*iwa.*n$\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataStartsWith = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"291\",\n" +
+ " \"name\": \"Criteria_EventTimeStamp_IsSet\",\n" +
+ " \"createdAt\": 1722497422151,\n" +
+ " \"updatedAt\": 1722500235276,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"StartsWith\",\n" +
+ " \"value\": \"T\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testEqualsMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 3\n," +
+ " \"savings\": 19.99\n," +
+ " \"likes_boba\": true\n," +
+ " \"country\": China\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataEquals, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testEqualsMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 4\n," +
+ " \"savings\": 20.99\n," +
+ " \"likes_boba\": true\n," +
+ " \"country\": China1\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataEquals, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testDoesNotEqualMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 105\n," +
+ " \"savings\": 105\n," +
+ " \"likes_boba\": true\n," +
+ " \"country\": China1\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataDoesNotEqual, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testDoesNotEqualMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 101\n," +
+ " \"savings\": 101\n," +
+ " \"likes_boba\": false\n," +
+ " \"country\": China\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataDoesNotEqual, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testGreaterThanMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 8\n," +
+ " \"savings\": 11\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataGreaterThan, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testGreaterThanMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 6\n," +
+ " \"savings\": 9\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataGreaterThan, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testGreaterThanOrEqualMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 13\n," +
+ " \"savings\": 21\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataGreaterThanOrEqualTo, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testGreaterThanOrEqualMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 11\n," +
+ " \"savings\": 19\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataGreaterThanOrEqualTo, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testLessThanMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 5\n," +
+ " \"savings\": 9\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataLessThan, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testLessThanMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 7\n," +
+ " \"savings\": 11\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataLessThan, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testLessThanOrEqualsToMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 56\n," +
+ " \"savings\": 60\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataLessThanOrEqualTo, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testLessThanOrEqualsToMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 57\n," +
+ " \"savings\": 61\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataLessThanOrEqualTo, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testIsSetMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\":\"10\"\n," +
+ " \"savings\":\"20\"\n," +
+ " \"country\":\"fwef\"\n," +
+ " \"saved_cars\":\"zdf\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataIsSet, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testIsSetMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\":\"12\"\n," +
+ " \"savings\":\"321\"\n," +
+ " \"country\":\"\"\n," +
+ " \"saved_cars\":\"\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataIsSet, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testContainsMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\":\"Taiwan\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataContains, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testContainsMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\":\"Canada\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataContains, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testMatchesRegexMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\":\"Taiwan\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMatchesRegex, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testMatchesRegexMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\":\"Canada\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMatchesRegex, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testStartsWithMockDataPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\":\"Taiwan\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataStartsWith, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testStartsWithMockDataFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\":\"Canada\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataStartsWith, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DoesNotEqualCriteriaMatchTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DoesNotEqualCriteriaMatchTest.java
new file mode 100644
index 000000000..819263e17
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/DoesNotEqualCriteriaMatchTest.java
@@ -0,0 +1,290 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DoesNotEqualCriteriaMatchTest {
+
+ static final String mockDataLongDoesNotEqual = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"194\",\n" +
+ " \"name\": \"Contact: Phone Number != 57688559\",\n" +
+ " \"createdAt\": 1721337331194,\n" +
+ " \"updatedAt\": 1722338525737,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"eventTimeStamp\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"15\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataStringDoesNotEqual = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"194\",\n" +
+ " \"name\": \"Contact: Phone Number != 57688559\",\n" +
+ " \"createdAt\": 1721337331194,\n" +
+ " \"updatedAt\": 1722338525737,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"phoneNumber\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"57688559\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataBooleanDoesNotEqual = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"194\",\n" +
+ " \"name\": \"Contact: Phone Number != 57688559\",\n" +
+ " \"createdAt\": 1721337331194,\n" +
+ " \"updatedAt\": 1722338525737,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"field\": \"subscribed\",\n" +
+ " \"fieldType\": \"boolean\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"id\": 25,\n" +
+ " \"value\": \"true\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ static final String mockDataDoubleDoesNotEqual = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"194\",\n" +
+ " \"name\": \"Contact: Phone Number != 57688559\",\n" +
+ " \"createdAt\": 1721337331194,\n" +
+ " \"updatedAt\": 1722338525737,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"savings\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"19.99\",\n" +
+ " \"fieldType\": \"double\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testCompareDataLongDoesNotEqualPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 17\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataLongDoesNotEqual, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataLongDoesNotEqualFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"eventTimeStamp\": 15\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataLongDoesNotEqual, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testCompareDataStringDoesNotEqualPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"phoneNumber\": \"5768855923\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataStringDoesNotEqual, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataStringDoesNotEqualFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"phoneNumber\": \"57688559\"\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataStringDoesNotEqual, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testCompareDataBooleanDoesNotEqualPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"subscribed\": false\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataBooleanDoesNotEqual, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataBooleanDoesNotEqualFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"subscribed\": true\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataBooleanDoesNotEqual, jsonArray);
+ assertFalse(result != null);
+ }
+
+ @Test
+ public void testCompareDataDoubleDoesNotEqualPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"savings\": 20.99\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataDoubleDoesNotEqual, jsonArray);
+ assertTrue(result != null);
+ }
+
+ @Test
+ public void testCompareDataDoubleDoesNotEqualFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"savings\": 19.99\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataDoubleDoesNotEqual, jsonArray);
+ assertFalse(result != null);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/IsOneOfAndIsNotOneOfCriteriaMatchTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/IsOneOfAndIsNotOneOfCriteriaMatchTest.java
new file mode 100644
index 000000000..f09c150a2
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/IsOneOfAndIsNotOneOfCriteriaMatchTest.java
@@ -0,0 +1,185 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class IsOneOfAndIsNotOneOfCriteriaMatchTest {
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ static final String mockDataCriteriaIsOneOf = "{\n" +
+ " \"count\": 5,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"299\",\n" +
+ " \"name\": \"Criteria_Is_One_of\",\n" +
+ " \"createdAt\": 1722851586508,\n" +
+ " \"updatedAt\": 1724404229481,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"values\": [\n" +
+ " \"China\",\n" +
+ " \"Japan\",\n" +
+ " \"Kenya\"\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"addresses\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"values\": [\n" +
+ " \"JP\",\n" +
+ " \"DE\",\n" +
+ " \"GB\"\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testIsOneOfPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": \"China\",\n" +
+ " \"addresses\": [\"US\",\"UK\",\"JP\",\"GB\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCriteriaIsOneOf, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testIsOneOfFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": \"Korea\",\n" +
+ " \"addresses\": [\"US\", \"UK\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCriteriaIsOneOf, jsonArray);
+ assertNull(result);
+ }
+
+ static final String mockDataCriteriaIsNotOneOf = "{\n" +
+ " \"count\": 5,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"299\",\n" +
+ " \"name\": \"Criteria_IsNonOf\",\n" +
+ " \"createdAt\": 1722851586508,\n" +
+ " \"updatedAt\": 1724404229481,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"country\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"values\": [\n" +
+ " \"China\",\n" +
+ " \"Japan\",\n" +
+ " \"Kenya\"\n" +
+ " ]\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"addresses\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"values\": [\n" +
+ " \"JP\",\n" +
+ " \"DE\",\n" +
+ " \"GB\"\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Test
+ public void testIsNotOneOfPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": \"Korea\",\n" +
+ " \"addresses\": [\"US\", \"UK\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCriteriaIsNotOneOf, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testIsNotOneOfFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"country\": \"China\",\n" +
+ " \"addresses\": [\"US\",\"UK\",\"JP\",\"GB\"]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataCriteriaIsNotOneOf, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaMatchTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaMatchTest.java
new file mode 100644
index 000000000..839b634bd
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaMatchTest.java
@@ -0,0 +1,179 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MultiLevelNestedCriteriaMatchTest {
+
+ static final String mockDataMultiLevelNested = "{\n" +
+ " \"count\": 3,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"425\",\n" +
+ " \"name\": \"Multi level Nested field criteria\",\n" +
+ " \"createdAt\": 1726811375306,\n" +
+ " \"updatedAt\": 1726811375306,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"button-clicked.updateCart.updatedShoppingCartItems.quantity\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"10\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"button-clicked.browserVisit.website.domain\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"https://mybrand.com/socks\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testMultiLevelNestedPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"updateCart\": {\n" +
+ " \"updatedShoppingCartItems\": {\n" +
+ " \"quantity\": 10\n" +
+ " }\n" +
+ " },\n" +
+ " \"browserVisit\": {\n" +
+ " \"website\": {\n" +
+ " \"domain\": \"https://mybrand.com/socks\"\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"eventName\": \"button-clicked\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMultiLevelNested, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testMultiLevelNestedFail1() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"updateCart\": {\n" +
+ " \"updatedShoppingCartItems\": {\n" +
+ " \"quantity\": 10\n" +
+ " }\n" +
+ " },\n" +
+ " \"browserVisit\": {\n" +
+ " \"website.domain\": \"https://mybrand.com/socks\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"eventName\": \"button-clicked\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMultiLevelNested, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testMultiLevelNestedFail2() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"updateCart\": {\n" +
+ " \"updatedShoppingCartItems.quantity\": 10\n" +
+ " },\n" +
+ " \"browserVisit\": {\n" +
+ " \"website.domain\": \"https://mybrand.com/socks\"\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"eventName\": \"button-clicked\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMultiLevelNested, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testMultiLevelNestedFail3() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"button-clicked\": {\n" +
+ " \"updateCart\": {\n" +
+ " \"updatedShoppingCartItems\": {\n" +
+ " \"quantity\": 10\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " \"browserVisit\": {\n" +
+ " \"website\": {\n" +
+ " \"domain\": \"https://mybrand.com/socks\"\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"eventName\": \"button-clicked\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMultiLevelNested, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testMultiLevelNestedFail4() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"quantity\": 10,\n" +
+ " \"domain\": \"https://mybrand.com/socks\"\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"eventName\": \"button-clicked\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMultiLevelNested, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaWithArrayMatchTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaWithArrayMatchTest.java
new file mode 100644
index 000000000..ccaf5120a
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/MultiLevelNestedCriteriaWithArrayMatchTest.java
@@ -0,0 +1,224 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MultiLevelNestedCriteriaWithArrayMatchTest {
+
+ static final String mockDataNestedMultiLevelArrayTrackEvent = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"459\",\n" +
+ " \"name\": \"event a.h.b=d && a.h.c=g\",\n" +
+ " \"createdAt\": 1727717997842,\n" +
+ " \"updatedAt\": 1728024187962,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"TopLevelArrayObject.a.h.b\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"d\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"TopLevelArrayObject.a.h.c\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"g\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }";
+
+ static final String mockDataMultiLevelNestedWithArray = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"436\",\n" +
+ " \"name\": \"Criteria 2.1 - 09252024 Bug Bash\",\n" +
+ " \"createdAt\": 1727286807360,\n" +
+ " \"updatedAt\": 1727445082036,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"furniture.material.type\",\n" +
+ " \"comparatorType\": \"Contains\",\n" +
+ " \"value\": \"table\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"furniture.material.color\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"values\": [\"black\"]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}\n";
+
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+
+ @Test
+ public void testMultiLevelNestedWithArrayPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"furniture\": {\n" +
+ " \"material\": [\n" +
+ " {\n" +
+ " \"type\": \"table\",\n" +
+ " \"color\": \"black\",\n" +
+ " \"lengthInches\": 40,\n" +
+ " \"widthInches\": 60\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"Sofa\",\n" +
+ " \"color\": \"Gray\",\n" +
+ " \"lengthInches\": 20,\n" +
+ " \"widthInches\": 30\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]\n";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMultiLevelNestedWithArray, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testMultiLevelNestedWithArrayFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"furniture\": {\n" +
+ " \"material\": [\n" +
+ " {\n" +
+ " \"type\": \"table\",\n" +
+ " \"color\": \"gray\",\n" +
+ " \"lengthInches\": 40,\n" +
+ " \"widthInches\": 60\n" +
+ " },\n" +
+ " {\n" +
+ " \"type\": \"Sofa\",\n" +
+ " \"color\": \"black\",\n" +
+ " \"lengthInches\": 20,\n" +
+ " \"widthInches\": 30\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ "]\n";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataMultiLevelNestedWithArray, jsonArray);
+ assertNull(result);
+ }
+
+ @Test
+ public void testNestedMultiLevelArrayTrackEventPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventName\": \"TopLevelArrayObject\",\n" +
+ " \"dataFields\": {\n" +
+ " \"a\": {\n" +
+ " \"h\": [\n" +
+ " {\n" +
+ " \"b\": \"e\",\n" +
+ " \"c\": \"h\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"b\": \"d\",\n" +
+ " \"c\": \"g\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataNestedMultiLevelArrayTrackEvent, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testNestedMultiLevelArrayTrackEventFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"eventName\": \"TopLevelArrayObject\",\n" +
+ " \"dataFields\": {\n" +
+ " \"a\": {\n" +
+ " \"h\": [\n" +
+ " {\n" +
+ " \"b\": \"d\",\n" +
+ " \"c\": \"h\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"b\": \"e\",\n" +
+ " \"c\": \"g\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataNestedMultiLevelArrayTrackEvent, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/NestedCriteriaMatchTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/NestedCriteriaMatchTest.java
new file mode 100644
index 000000000..2247f91b8
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/NestedCriteriaMatchTest.java
@@ -0,0 +1,129 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class NestedCriteriaMatchTest {
+
+ static final String mockDataNested = " {\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"168\",\n" +
+ " \"name\": \"nested testing\",\n" +
+ " \"createdAt\": 1721251169153,\n" +
+ " \"updatedAt\": 1723488175352,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"furniture\",\n" +
+ " \"comparatorType\": \"IsSet\",\n" +
+ " \"value\": \"\",\n" +
+ " \"fieldType\": \"nested\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"furniture.furnitureColor\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"White\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"user\",\n" +
+ " \"field\": \"furniture.furnitureType\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"Sofa\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }";
+
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testNestedPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"furniture\": [\n" +
+ " {\n" +
+ " \"furnitureType\": \"Sofa\",\n" +
+ " \"furnitureColor\": \"White\",\n" +
+ " \"lengthInches\": 40,\n" +
+ " \"widthInches\": 60\n" +
+ " },\n" +
+ " {\n" +
+ " \"furnitureType\": \"Table\",\n" +
+ " \"furnitureColor\": \"Gray\",\n" +
+ " \"lengthInches\": 20,\n" +
+ " \"widthInches\": 30\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ " ]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataNested, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testNestedFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"furniture\": [\n" +
+ " {\n" +
+ " \"furnitureType\": \"Sofa\",\n" +
+ " \"furnitureColor\": \"Gray\",\n" +
+ " \"lengthInches\": 40,\n" +
+ " \"widthInches\": 60\n" +
+ " },\n" +
+ " {\n" +
+ " \"furnitureType\": \"Table\",\n" +
+ " \"furnitureColor\": \"White\",\n" +
+ " \"lengthInches\": 20,\n" +
+ " \"widthInches\": 30\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " \"eventType\": \"user\"\n" +
+ " }\n" +
+ " ]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataNested, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/SinglePrimitiveArrayNestedCriteriaMatchTest.java b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/SinglePrimitiveArrayNestedCriteriaMatchTest.java
new file mode 100644
index 000000000..4b9ed846b
--- /dev/null
+++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/SinglePrimitiveArrayNestedCriteriaMatchTest.java
@@ -0,0 +1,98 @@
+package com.iterable.iterableapi.util;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SinglePrimitiveArrayNestedCriteriaMatchTest {
+
+ static final String mockDataArrayNested = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"467\",\n" +
+ " \"name\": \"Custom event - single primitive\",\n" +
+ " \"createdAt\": 1728166585122,\n" +
+ " \"updatedAt\": 1729581351423,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"animal_found\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal_found.count\",\n" +
+ " \"comparatorType\": \"DoesNotEqual\",\n" +
+ " \"value\": \"4\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+
+ private CriteriaCompletionChecker evaluator;
+
+ @Before
+ public void setUp() {
+ evaluator = new CriteriaCompletionChecker();
+ }
+
+ @Test
+ public void testSinglePrimitiveArrayNestedCriteriaMatchPass() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"count\": [5,8,9]\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"eventName\": \"animal_found\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayNested, jsonArray);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testSinglePrimitiveArrayNestedCriteriaMatchFail() throws Exception {
+ String jsonString = "[\n" +
+ " {\n" +
+ " \"dataFields\": {\n" +
+ " \"count\": [4, 8, 9]\n" +
+ " },\n" +
+ " \"eventType\": \"customEvent\",\n" +
+ " \"eventName\": \"animal_found\"\n" +
+ " }\n" +
+ "]";
+ JSONArray jsonArray = new JSONArray(jsonString);
+ String result = evaluator.getMatchedCriteria(mockDataArrayNested, jsonArray);
+ assertNull(result);
+ }
+}
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java
index 8a2165cbb..7ce908c4e 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java
@@ -19,6 +19,7 @@
import java.util.HashMap;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
/**
@@ -35,6 +36,7 @@ public class IterableApi {
private String _apiKey;
private String _email;
private String _userId;
+ String _userIdUnknown;
private String _authToken;
private boolean _debugMode;
private Bundle _payloadData;
@@ -45,6 +47,8 @@ public class IterableApi {
private IterableHelper.FailureHandler _setUserFailureCallbackHandler;
IterableApiClient apiClient = new IterableApiClient(new IterableApiAuthProvider());
+ private static final UnknownUserMerge unknownUserMerge = new UnknownUserMerge();
+ private @Nullable UnknownUserManager unknownUserManager;
private @Nullable IterableInAppManager inAppManager;
private @Nullable IterableEmbeddedManager embeddedManager;
private String inboxSessionId;
@@ -350,7 +354,15 @@ private void logoutPreviousUser() {
apiClient.onLogout();
}
- private void onLogin(@Nullable String authToken) {
+ private void onLogin(
+ @Nullable String authToken,
+ String userIdOrEmail,
+ boolean isEmail,
+ boolean merge,
+ boolean replay,
+ boolean isUnknown,
+ @Nullable IterableHelper.FailureHandler failureHandler
+ ) {
if (!isInitialized()) {
setAuthToken(null);
return;
@@ -359,8 +371,9 @@ private void onLogin(@Nullable String authToken) {
getAuthManager().pauseAuthRetries(false);
if (authToken != null) {
setAuthToken(authToken);
+ attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler);
} else {
- getAuthManager().requestNewAuthToken(false);
+ getAuthManager().requestNewAuthToken(false, data -> attemptMergeAndEventReplay(userIdOrEmail, isEmail, merge, replay, isUnknown, failureHandler));
}
}
@@ -424,7 +437,7 @@ private boolean isInitialized() {
return _apiKey != null && (_email != null || _userId != null);
}
- private boolean checkSDKInitialization() {
+ boolean checkSDKInitialization() {
if (!isInitialized()) {
IterableLogger.w(TAG, "Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods");
return false;
@@ -455,6 +468,7 @@ private void storeAuthData() {
if (iterableKeychain != null) {
iterableKeychain.saveEmail(_email);
iterableKeychain.saveUserId(_userId);
+ iterableKeychain.saveUserIdUnknown(_userIdUnknown);
iterableKeychain.saveAuthToken(_authToken);
} else {
IterableLogger.e(TAG, "Shared preference creation failed. ");
@@ -469,6 +483,7 @@ private void retrieveEmailAndUserId() {
if (iterableKeychain != null) {
_email = iterableKeychain.getEmail();
_userId = iterableKeychain.getUserId();
+ _userIdUnknown = iterableKeychain.getUserIdUnknown();
_authToken = iterableKeychain.getAuthToken();
} else {
IterableLogger.e(TAG, "retrieveEmailAndUserId: Shared preference creation failed. Could not retrieve email/userId");
@@ -497,6 +512,12 @@ public String getUserId() {
return _userId;
}
+ @Nullable
+ @Override
+ public String getUserIdUnknown() {
+ return _userIdUnknown;
+ }
+
@Nullable
@Override
public String getAuthToken() {
@@ -542,6 +563,12 @@ void setAuthToken(String authToken, boolean bypassAuth) {
protected void registerDeviceToken(final @Nullable String email, final @Nullable String userId, final @Nullable String authToken, final @NonNull String applicationName, final @NonNull String deviceToken, final HashMap deviceAttributes) {
if (deviceToken != null) {
+ if (!checkSDKInitialization() && _userIdUnknown == null) {
+ if (sharedInstance.config.enableUnknownUserActivation) {
+ unknownUserManager.trackUnknownTokenRegistration(deviceToken);
+ }
+ return;
+ }
final Thread registrationThread = new Thread(new Runnable() {
public void run() {
registerDeviceToken(email, userId, authToken, applicationName, deviceToken, null, deviceAttributes);
@@ -592,7 +619,40 @@ protected void registerDeviceToken(@Nullable String email, @Nullable String user
IterableLogger.e(TAG, "registerDeviceToken: applicationName is null, check that pushIntegrationName is set in IterableConfig");
}
- apiClient.registerDeviceToken(email, userId, authToken, applicationName, deviceToken, dataFields, deviceAttributes, _setUserSuccessCallbackHandler, _setUserFailureCallbackHandler);
+ IterableHelper.SuccessHandler wrappedSuccessHandler = getSuccessHandler();
+ IterableHelper.FailureHandler wrappedFailureHandler = getFailureHandler();
+
+ apiClient.registerDeviceToken(email, userId, authToken, applicationName, deviceToken, dataFields, deviceAttributes, wrappedSuccessHandler, wrappedFailureHandler);
+ }
+
+ private IterableHelper.SuccessHandler getSuccessHandler() {
+ IterableHelper.SuccessHandler wrappedSuccessHandler = null;
+ if (_setUserSuccessCallbackHandler != null || (config.enableUnknownUserActivation && getVisitorUsageTracked() && config.identityResolution.getReplayOnVisitorToKnown())) {
+ final IterableHelper.SuccessHandler originalSuccessHandler = _setUserSuccessCallbackHandler;
+ wrappedSuccessHandler = data -> {
+ trackConsentOnDeviceRegistration();
+
+ if (originalSuccessHandler != null) {
+ originalSuccessHandler.onSuccess(data);
+ }
+ };
+ }
+ return wrappedSuccessHandler;
+ }
+
+ private IterableHelper.FailureHandler getFailureHandler() {
+ IterableHelper.FailureHandler wrappedFailureHandler = null;
+ if (_setUserFailureCallbackHandler != null || (config.enableUnknownUserActivation && getVisitorUsageTracked() && config.identityResolution.getReplayOnVisitorToKnown())) {
+ final IterableHelper.FailureHandler originalFailureHandler = _setUserFailureCallbackHandler;
+ wrappedFailureHandler = (reason, data) -> {
+ trackConsentOnDeviceRegistration();
+
+ if (originalFailureHandler != null) {
+ originalFailureHandler.onFailure(reason, data);
+ }
+ };
+ }
+ return wrappedFailureHandler;
}
//endregion
@@ -636,16 +696,32 @@ public static void initialize(@NonNull Context context, @NonNull String apiKey,
);
}
+ if (sharedInstance.unknownUserManager == null) {
+ sharedInstance.unknownUserManager = new UnknownUserManager(
+ sharedInstance
+ );
+ }
+
loadLastSavedConfiguration(context);
+ IterablePushNotificationUtil.processPendingAction(context);
+
+ if (!sharedInstance.checkSDKInitialization()
+ && sharedInstance._userIdUnknown == null
+ && sharedInstance.config.enableUnknownUserActivation
+ && sharedInstance.getVisitorUsageTracked()) {
+ sharedInstance.unknownUserManager.updateUnknownSession();
+ sharedInstance.unknownUserManager.getCriteria();
+ }
+
if (DeviceInfoUtils.isFireTV(context.getPackageManager())) {
try {
JSONObject dataFields = new JSONObject();
JSONObject deviceDetails = new JSONObject();
DeviceInfoUtils.populateDeviceDetails(deviceDetails, context, sharedInstance.getDeviceId(), null);
dataFields.put(IterableConstants.KEY_FIRETV, deviceDetails);
- sharedInstance.apiClient.updateUser(dataFields, false);
+ sharedInstance.updateUser(dataFields, false);
} catch (JSONException e) {
- IterableLogger.e(TAG, "initialize: exception", e);
+ IterableLogger.e(TAG, "initialize: exception", e);
}
}
}
@@ -726,24 +802,42 @@ public IterableAttributionInfo getAttributionInfo() {
public void pauseAuthRetries(boolean pauseRetry) {
getAuthManager().pauseAuthRetries(pauseRetry);
if (!pauseRetry) { // request new auth token as soon as unpause
- getAuthManager().requestNewAuthToken(false);
+ getAuthManager().requestNewAuthToken(false, null);
}
}
public void setEmail(@Nullable String email) {
- setEmail(email, null, null, null);
+ setEmail(email, null, null, null, null);
+ }
+
+ public void setEmail(@Nullable String email, IterableIdentityResolution identityResolution) {
+ setEmail(email, null, identityResolution, null, null);
}
public void setEmail(@Nullable String email, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
- setEmail(email, null, successHandler, failureHandler);
+ setEmail(email, null, null, successHandler, failureHandler);
+ }
+
+ public void setEmail(@Nullable String email, IterableIdentityResolution identityResolution, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
+ setEmail(email, null, identityResolution, successHandler, failureHandler);
}
public void setEmail(@Nullable String email, @Nullable String authToken) {
- setEmail(email, authToken, null, null);
+ setEmail(email, authToken, null, null, null);
+ }
+
+ public void setEmail(@Nullable String email, @Nullable String authToken, IterableIdentityResolution identityResolution) {
+ setEmail(email, authToken, identityResolution, null, null);
}
public void setEmail(@Nullable String email, @Nullable String authToken, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
- //Only if passed in same non-null email
+ setEmail(email, authToken, null, successHandler, failureHandler);
+ }
+
+ public void setEmail(@Nullable String email, @Nullable String authToken, @Nullable IterableIdentityResolution iterableIdentityResolution, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
+ boolean replay = isReplay(iterableIdentityResolution);
+ boolean merge = isMerge(iterableIdentityResolution);
+
if (_email != null && _email.equals(email)) {
checkAndUpdateAuthToken(authToken);
return;
@@ -757,27 +851,63 @@ public void setEmail(@Nullable String email, @Nullable String authToken, @Nullab
_email = email;
_userId = null;
+
+ if (config.authHandler == null) {
+ attemptMergeAndEventReplay(email, true, merge, replay, false, failureHandler);
+ }
+
+ if (email == null) {
+ unknownUserManager.setCriteriaMatched(false);
+ _userIdUnknown = null;
+ setConsentLogged(false);
+ }
+
_setUserSuccessCallbackHandler = successHandler;
_setUserFailureCallbackHandler = failureHandler;
storeAuthData();
- onLogin(authToken);
+ onLogin(authToken, email, true, merge, replay, false, failureHandler);
+ }
+
+ public void setUnknownUser(@Nullable String userId) {
+ _userIdUnknown = userId;
+ setUserId(userId, null, null, null, null, true);
+ storeAuthData();
}
public void setUserId(@Nullable String userId) {
- setUserId(userId, null, null, null);
+ setUserId(userId, null, null, null, null, false);
+ }
+
+ public void setUserId(@Nullable String userId, IterableIdentityResolution identityResolution) {
+ setUserId(userId, null, identityResolution, null, null, false);
}
public void setUserId(@Nullable String userId, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
- setUserId(userId, null, successHandler, failureHandler);
+ setUserId(userId, null, null, successHandler, failureHandler, false);
+ }
+
+ public void setUserId(@Nullable String userId, IterableIdentityResolution identityResolution, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
+ setUserId(userId, null, identityResolution, successHandler, failureHandler, false);
}
public void setUserId(@Nullable String userId, @Nullable String authToken) {
- setUserId(userId, authToken, null, null);
+ setUserId(userId, authToken, null, null, null, false);
+ }
+
+ public void setUserId(@Nullable String userId, @Nullable String authToken, IterableIdentityResolution identityResolution) {
+ setUserId(userId, authToken, identityResolution, null, null, false);
+
}
public void setUserId(@Nullable String userId, @Nullable String authToken, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
- //If same non null userId is passed
+ setUserId(userId, authToken, null, successHandler, failureHandler, false);
+ }
+
+ public void setUserId(@Nullable String userId, @Nullable String authToken, @Nullable IterableIdentityResolution iterableIdentityResolution, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler, boolean isUnknown) {
+ boolean replay = isReplay(iterableIdentityResolution);
+ boolean merge = isMerge(iterableIdentityResolution);
+
if (_userId != null && _userId.equals(userId)) {
checkAndUpdateAuthToken(authToken);
return;
@@ -791,11 +921,59 @@ public void setUserId(@Nullable String userId, @Nullable String authToken, @Null
_email = null;
_userId = userId;
+
+ if (config.authHandler == null) {
+ attemptMergeAndEventReplay(userId, false, merge, replay, isUnknown, failureHandler);
+ }
+
+ if (userId == null) {
+ unknownUserManager.setCriteriaMatched(false);
+ _userIdUnknown = null;
+ setConsentLogged(false);
+ }
+
_setUserSuccessCallbackHandler = successHandler;
_setUserFailureCallbackHandler = failureHandler;
storeAuthData();
- onLogin(authToken);
+ onLogin(authToken, userId, false, merge, replay, isUnknown, failureHandler);
+ }
+
+ private boolean isMerge(@Nullable IterableIdentityResolution iterableIdentityResolution) {
+ return (iterableIdentityResolution != null) ? iterableIdentityResolution.getMergeOnUnknownToKnown() : config.identityResolution.getMergeOnUnknownToKnown();
+ }
+
+ private boolean isReplay(@Nullable IterableIdentityResolution iterableIdentityResolution) {
+ return (iterableIdentityResolution != null) ? iterableIdentityResolution.getReplayOnVisitorToKnown() : config.identityResolution.getReplayOnVisitorToKnown();
+ }
+
+ private void attemptMergeAndEventReplay(@Nullable String emailOrUserId, boolean isEmail, boolean merge, boolean replay, boolean isUnknown, IterableHelper.FailureHandler failureHandler) {
+ if (config.enableUnknownUserActivation && getVisitorUsageTracked()) {
+
+ if (emailOrUserId != null && !emailOrUserId.equals(_userIdUnknown)) {
+ attemptAndProcessMerge(emailOrUserId, isEmail, merge, failureHandler, _userIdUnknown);
+ }
+
+ if (replay && (_userId != null || _email != null)) {
+ unknownUserManager.syncEventsAndUserUpdate();
+ }
+
+ if (!isUnknown) {
+ _userIdUnknown = null;
+ }
+
+ unknownUserManager.clearVisitorEventsAndUserData();
+ }
+ }
+
+ private void attemptAndProcessMerge(@NonNull String destinationUser, boolean isEmail, boolean merge, IterableHelper.FailureHandler failureHandler, String unknownUserId) {
+ unknownUserMerge.tryMergeUser(apiClient, unknownUserId, destinationUser, isEmail, merge, (mergeResult, error) -> {
+ if (!(Objects.equals(mergeResult, IterableConstants.MERGE_SUCCESSFUL) || Objects.equals(mergeResult, IterableConstants.MERGE_NOTREQUIRED))) {
+ if (failureHandler != null) {
+ failureHandler.onFailure(error, null);
+ }
+ }
+ });
}
public void setAuthToken(String authToken) {
@@ -1055,7 +1233,10 @@ public void track(@NonNull String eventName, int campaignId, int templateId) {
*/
public void track(@NonNull String eventName, int campaignId, int templateId, @Nullable JSONObject dataFields) {
IterableLogger.printInfo();
- if (!checkSDKInitialization()) {
+ if (!checkSDKInitialization() && _userIdUnknown == null) {
+ if (sharedInstance.config.enableUnknownUserActivation) {
+ unknownUserManager.trackUnknownEvent(eventName, dataFields);
+ }
return;
}
@@ -1067,7 +1248,10 @@ public void track(@NonNull String eventName, int campaignId, int templateId, @Nu
* @param items
*/
public void updateCart(@NonNull List items) {
- if (!checkSDKInitialization()) {
+ if (!checkSDKInitialization() && _userIdUnknown == null) {
+ if (sharedInstance.config.enableUnknownUserActivation) {
+ unknownUserManager.trackUnknownUpdateCart(items);
+ }
return;
}
@@ -1090,13 +1274,10 @@ public void trackPurchase(double total, @NonNull List items) {
* @param dataFields a `JSONObject` containing any additional information to save along with the event
*/
public void trackPurchase(double total, @NonNull List items, @Nullable JSONObject dataFields) {
- if (!checkSDKInitialization()) {
- return;
- }
-
- apiClient.trackPurchase(total, items, dataFields, null);
+ trackPurchase(total, items, dataFields, null);
}
+
/**
* Tracks a purchase.
* @param total total purchase amount
@@ -1105,7 +1286,10 @@ public void trackPurchase(double total, @NonNull List items, @Null
* @param attributionInfo a `JSONObject` containing information about what the purchase was attributed to
*/
public void trackPurchase(double total, @NonNull List items, @Nullable JSONObject dataFields, @Nullable IterableAttributionInfo attributionInfo) {
- if (!checkSDKInitialization()) {
+ if (!checkSDKInitialization() && _userIdUnknown == null) {
+ if (sharedInstance.config.enableUnknownUserActivation) {
+ unknownUserManager.trackUnknownPurchaseEvent(total, items, dataFields);
+ }
return;
}
@@ -1157,7 +1341,7 @@ public void onSuccess(@NonNull JSONObject data) {
}
storeAuthData();
- getAuthManager().requestNewAuthToken(false);
+ getAuthManager().requestNewAuthToken(false, null);
if (successHandler != null) {
successHandler.onSuccess(data);
@@ -1180,7 +1364,10 @@ public void updateUser(@NonNull JSONObject dataFields) {
* @param mergeNestedObjects
*/
public void updateUser(@NonNull JSONObject dataFields, Boolean mergeNestedObjects) {
- if (!checkSDKInitialization()) {
+ if (!checkSDKInitialization() && _userIdUnknown == null) {
+ if (sharedInstance.config.enableUnknownUserActivation) {
+ unknownUserManager.trackUnknownUpdateUser(dataFields);
+ }
return;
}
@@ -1302,6 +1489,87 @@ public void trackEmbeddedClick(@NonNull IterableEmbeddedMessage message, @Nullab
apiClient.trackEmbeddedClick(message, buttonIdentifier, clickedUrl);
}
+ public void setVisitorUsageTracked(@NonNull Boolean isSetVisitorUsageTracked) {
+ SharedPreferences sharedPref = sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, "");
+ editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ editor.putString(IterableConstants.SHARED_PREFS_USER_UPDATE_OBJECT_KEY, "");
+ editor.putString(IterableConstants.SHARED_PREFS_CRITERIA, "");
+ editor.putBoolean(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED, isSetVisitorUsageTracked);
+
+ if (isSetVisitorUsageTracked) {
+ editor.putLong(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED_TIME, IterableUtil.currentTimeMillis());
+ } else {
+ editor.remove(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED_TIME);
+ }
+
+ editor.apply();
+
+ if (isSetVisitorUsageTracked && config.enableUnknownUserActivation) {
+ unknownUserManager.updateUnknownSession();
+ unknownUserManager.getCriteria();
+ }
+ }
+
+ public boolean getVisitorUsageTracked() {
+ SharedPreferences sharedPreferences = sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ return sharedPreferences.getBoolean(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED, false);
+ }
+
+ private boolean getConsentLogged() {
+ if (_applicationContext == null) {
+ return false;
+ }
+ SharedPreferences sharedPreferences = _applicationContext.getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ return sharedPreferences.getBoolean(IterableConstants.SHARED_PREFS_CONSENT_LOGGED, false);
+ }
+
+ private void setConsentLogged(boolean consentLogged) {
+ if (_applicationContext == null) {
+ return;
+ }
+ SharedPreferences sharedPref = _applicationContext.getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ if (consentLogged) {
+ editor.putBoolean(IterableConstants.SHARED_PREFS_CONSENT_LOGGED, true);
+ } else {
+ editor.remove(IterableConstants.SHARED_PREFS_CONSENT_LOGGED);
+ }
+ editor.apply();
+ }
+
+ /**
+ * Tracks consent during device registration if conditions are met.
+ */
+ private void trackConsentOnDeviceRegistration() {
+ if (config.enableUnknownUserActivation && getVisitorUsageTracked() && config.identityResolution.getReplayOnVisitorToKnown()) {
+ if (!getConsentLogged()) {
+ boolean isUserKnown = (_userIdUnknown == null);
+ trackConsentForUser(_email, _userId, isUserKnown);
+ setConsentLogged(true);
+ }
+ }
+ }
+
+ /**
+ * Tracks user consent with the timestamp from when visitor usage was first tracked.
+ * This should be called when transitioning from unknown to known user.
+ * @param email User email (if available)
+ * @param userId User ID (if available)
+ * @param isUserKnown true if user is signing in (known user), false if user meets criteria and ID is generated
+ */
+ void trackConsentForUser(@Nullable String email, @Nullable String userId, boolean isUserKnown) {
+ if (!config.enableUnknownUserActivation || !getVisitorUsageTracked()) {
+ return;
+ }
+
+ SharedPreferences sharedPref = getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ Long timeOfConsent = sharedPref.getLong(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED_TIME, IterableUtil.currentTimeMillis());
+
+ apiClient.trackConsent(userId, email, timeOfConsent, isUserKnown);
+ }
+
//endregion
//region DEPRECATED - API public functions
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java
index 1afbffedb..bdb3a2578 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java
@@ -28,6 +28,8 @@ interface AuthProvider {
@Nullable
String getUserId();
@Nullable
+ String getUserIdUnknown();
+ @Nullable
String getAuthToken();
@Nullable
String getApiKey();
@@ -35,7 +37,6 @@ interface AuthProvider {
String getDeviceId();
@Nullable
Context getContext();
-
void resetAuth();
}
@@ -97,6 +98,31 @@ public void track(@NonNull String eventName, int campaignId, int templateId, @Nu
}
}
+ public void track(@NonNull String eventName, int campaignId, int templateId, @Nullable JSONObject dataFields, String createdAt) {
+ JSONObject requestJSON = new JSONObject();
+ try {
+ addEmailOrUserIdToJson(requestJSON);
+ requestJSON.put(IterableConstants.KEY_EVENT_NAME, eventName);
+
+ if (campaignId != 0) {
+ requestJSON.put(IterableConstants.KEY_CAMPAIGN_ID, campaignId);
+ }
+ if (templateId != 0) {
+ requestJSON.put(IterableConstants.KEY_TEMPLATE_ID, templateId);
+ }
+ if (dataFields != null) {
+ dataFields.remove(IterableConstants.SHARED_PREFS_EVENT_TYPE);
+ dataFields.remove(IterableConstants.KEY_EVENT_NAME);
+ }
+ requestJSON.put(IterableConstants.KEY_CREATED_AT, createdAt);
+ requestJSON.put(IterableConstants.KEY_DATA_FIELDS, dataFields);
+ requestJSON.put(IterableConstants.KEY_CREATE_NEW_FIELDS, true);
+ sendPostRequest(IterableConstants.ENDPOINT_TRACK, requestJSON);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
public void updateCart(@NonNull List items) {
JSONObject requestJSON = new JSONObject();
@@ -118,6 +144,28 @@ public void updateCart(@NonNull List items) {
}
}
+ public void updateCart(@NonNull List items, long createdAt) {
+ JSONObject requestJSON = new JSONObject();
+
+ try {
+ JSONArray itemsArray = new JSONArray();
+ for (CommerceItem item : items) {
+ itemsArray.put(item.toJSONObject());
+ }
+
+ JSONObject userObject = new JSONObject();
+ addEmailOrUserIdToJson(userObject);
+ userObject.put(IterableConstants.KEY_PREFER_USER_ID, true);
+ requestJSON.put(IterableConstants.KEY_USER, userObject);
+ requestJSON.put(IterableConstants.KEY_ITEMS, itemsArray);
+ requestJSON.put(IterableConstants.KEY_CREATED_AT, createdAt);
+
+ sendPostRequest(IterableConstants.ENDPOINT_UPDATE_CART, requestJSON);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
public void trackPurchase(double total, @NonNull List items, @Nullable JSONObject dataFields, @Nullable IterableAttributionInfo attributionInfo) {
JSONObject requestJSON = new JSONObject();
try {
@@ -147,6 +195,33 @@ public void trackPurchase(double total, @NonNull List items, @Null
}
}
+ public void trackPurchase(double total, @NonNull List items, @Nullable JSONObject dataFields, @NonNull long createdAt) {
+ JSONObject requestJSON = new JSONObject();
+ try {
+ JSONArray itemsArray = new JSONArray();
+ for (CommerceItem item : items) {
+ itemsArray.put(item.toJSONObject());
+ }
+
+ JSONObject userObject = new JSONObject();
+ addEmailOrUserIdToJson(userObject);
+ userObject.put(IterableConstants.KEY_PREFER_USER_ID, true);
+ requestJSON.put(IterableConstants.KEY_USER, userObject);
+
+ requestJSON.put(IterableConstants.KEY_ITEMS, itemsArray);
+ requestJSON.put(IterableConstants.KEY_TOTAL, total);
+ requestJSON.put(IterableConstants.KEY_CREATED_AT, createdAt);
+ if (dataFields != null) {
+ dataFields.remove(IterableConstants.SHARED_PREFS_EVENT_TYPE);
+ requestJSON.put(IterableConstants.KEY_DATA_FIELDS, dataFields);
+ }
+
+ sendPostRequest(IterableConstants.ENDPOINT_TRACK_PURCHASE, requestJSON);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
public void updateEmail(final @NonNull String newEmail, final @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
JSONObject requestJSON = new JSONObject();
@@ -171,7 +246,7 @@ public void updateUser(@NonNull JSONObject dataFields, Boolean mergeNestedObject
addEmailOrUserIdToJson(requestJSON);
// Create the user by userId if it doesn't exist
- if (authProvider.getEmail() == null && authProvider.getUserId() != null) {
+ if (authProvider.getEmail() == null && (authProvider.getUserIdUnknown() != null || authProvider.getUserId() != null)) {
requestJSON.put(IterableConstants.KEY_PREFER_USER_ID, true);
}
@@ -605,7 +680,11 @@ private void addEmailOrUserIdToJson(JSONObject requestJSON) {
if (authProvider.getEmail() != null) {
requestJSON.put(IterableConstants.KEY_EMAIL, authProvider.getEmail());
} else {
- requestJSON.put(IterableConstants.KEY_USER_ID, authProvider.getUserId());
+ if (authProvider.getUserIdUnknown() != null) {
+ requestJSON.put(IterableConstants.KEY_USER_ID, authProvider.getUserIdUnknown());
+ } else {
+ requestJSON.put(IterableConstants.KEY_USER_ID, authProvider.getUserId());
+ }
}
} catch (JSONException e) {
e.printStackTrace();
@@ -703,4 +782,70 @@ void onLogout() {
getRequestProcessor().onLogout(authProvider.getContext());
authProvider.resetAuth();
}
-}
\ No newline at end of file
+
+ void mergeUser(String sourceEmail, String sourceUserId, String destinationEmail, String destinationUserId, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) {
+ JSONObject requestJson = new JSONObject();
+ try {
+ if (sourceEmail != null && !sourceEmail.isEmpty()) {
+ requestJson.put(IterableConstants.SOURCE_EMAIL, sourceEmail);
+ }
+ if (sourceUserId != null && !sourceUserId.isEmpty()) {
+ requestJson.put(IterableConstants.SOURCE_USER_ID, sourceUserId);
+ }
+ if (destinationEmail != null && !destinationEmail.isEmpty()) {
+ requestJson.put(IterableConstants.DESTINATION_EMAIL, destinationEmail);
+ }
+ if (destinationUserId != null && !destinationUserId.isEmpty()) {
+ requestJson.put(IterableConstants.DESTINATION_USER_ID, destinationUserId);
+ }
+ sendPostRequest(IterableConstants.ENDPOINT_MERGE_USER, requestJson, successHandler, failureHandler);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void getCriteriaList(@Nullable IterableHelper.IterableActionHandler actionHandler) {
+ sendGetRequest(IterableConstants.ENDPOINT_CRITERIA_LIST, new JSONObject(), actionHandler);
+ }
+
+ void trackUnknownUserSession(long createdAt, String userId, @NonNull JSONObject requestJson, JSONObject updateUserTrack, @NonNull IterableHelper.SuccessHandler onSuccess, @NonNull IterableHelper.FailureHandler onFailure) {
+ try {
+ JSONObject requestObject = new JSONObject();
+
+ JSONObject userObject = new JSONObject();
+ userObject.put(IterableConstants.KEY_USER_ID, userId);
+ userObject.put(IterableConstants.KEY_PREFER_USER_ID, true);
+ userObject.put(IterableConstants.KEY_MERGE_NESTED_OBJECTS, true);
+ userObject.put(IterableConstants.KEY_CREATE_NEW_FIELDS, true);
+ if (updateUserTrack != null) {
+ userObject.put(IterableConstants.KEY_DATA_FIELDS, updateUserTrack);
+ }
+ requestObject.put(IterableConstants.KEY_USER, userObject);
+ requestObject.put(IterableConstants.KEY_CREATED_AT, createdAt);
+ requestObject.put(IterableConstants.KEY_DEVICE_INFO, getDeviceInfoJson());
+ requestObject.put(IterableConstants.KEY_UNKNOWN_SESSION_CONTEXT, requestJson);
+ sendPostRequest(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, requestObject, onSuccess, onFailure);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void trackConsent(@Nullable String userId, @Nullable String email, @NonNull Long timestamp, boolean isUserKnown) {
+ JSONObject requestJSON = new JSONObject();
+
+ try {
+ requestJSON.put(IterableConstants.KEY_CONSENT_TIMESTAMP, timestamp);
+ if (email != null) {
+ requestJSON.put(IterableConstants.KEY_EMAIL, email);
+ }
+ if (userId != null) {
+ requestJSON.put(IterableConstants.KEY_USER_ID, userId);
+ }
+ requestJSON.put(IterableConstants.KEY_IS_USER_KNOWN, isUserKnown);
+ requestJSON.put(IterableConstants.KEY_DEVICE_INFO, getDeviceInfoJson());
+ sendPostRequest(IterableConstants.ENDPOINT_TRACK_CONSENT, requestJSON);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableAuthManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableAuthManager.java
index 8585a9ae4..915dbbb2a 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableAuthManager.java
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableAuthManager.java
@@ -45,8 +45,8 @@ public class IterableAuthManager implements IterableActivityMonitor.AppStateCall
this.activityMonitor.addCallback(this);
}
- public synchronized void requestNewAuthToken(boolean hasFailedPriorAuth) {
- requestNewAuthToken(hasFailedPriorAuth, null, true);
+ public synchronized void requestNewAuthToken(boolean hasFailedPriorAuth, IterableHelper.SuccessHandler successCallback) {
+ requestNewAuthToken(hasFailedPriorAuth, successCallback, true);
}
public void pauseAuthRetries(boolean pauseRetry) {
@@ -132,17 +132,18 @@ public void run() {
private void handleAuthTokenSuccess(String authToken, IterableHelper.SuccessHandler successCallback) {
if (authToken != null) {
+ IterableApi.getInstance().setAuthToken(authToken);
+ queueExpirationRefresh(authToken);
+
if (successCallback != null) {
handleSuccessForAuthToken(authToken, successCallback);
}
- queueExpirationRefresh(authToken);
} else {
handleAuthFailure(authToken, AuthFailureReason.AUTH_TOKEN_NULL);
IterableApi.getInstance().setAuthToken(authToken);
scheduleAuthTokenRefresh(getNextRetryInterval(), false, null);
return;
}
- IterableApi.getInstance().setAuthToken(authToken);
reSyncAuth();
authHandler.onTokenRegistrationSuccessful(authToken);
}
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java
index 3c2d79641..2725c3de9 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConfig.java
@@ -61,6 +61,11 @@ public class IterableConfig {
*/
final IterableAuthHandler authHandler;
+ /**
+ * Handler that can be used to retrieve the unknown user id
+ */
+ final IterableUnknownUserHandler iterableUnknownUserHandler;
+
/**
* Duration prior to an auth expiration that a new auth token should be requested.
*/
@@ -88,6 +93,24 @@ public class IterableConfig {
*/
final boolean useInMemoryStorageForInApps;
+ final boolean encryptionEnforced;
+
+ /**
+ * Enables unknown user activation
+ */
+ final boolean enableUnknownUserActivation;
+
+ /**
+ * Toggles fetching of unknown user criteria on foregrounding when set to true
+ * By default, the SDK will fetch unknown user criteria on foregrounding.
+ */
+ final boolean enableForegroundCriteriaFetch;
+
+ /**
+ * The number of unknown user events stored in local storage
+ */
+ final int eventThresholdLimit;
+
/**
* Allows for fetching embedded messages.
*/
@@ -99,6 +122,12 @@ public class IterableConfig {
*/
final boolean keychainEncryption;
+ /**
+ * This controls whether the SDK should allow event replay from local storage to logged in profile
+ * and merging between the generated unknown user profile and the logged in profile by default.
+ */
+ final IterableIdentityResolution identityResolution;
+
/**
* Handler for decryption failures of PII information.
* Before calling this handler, the SDK will clear the PII information and create new encryption keys
@@ -126,8 +155,14 @@ private IterableConfig(Builder builder) {
allowedProtocols = builder.allowedProtocols;
dataRegion = builder.dataRegion;
useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps;
+ encryptionEnforced = builder.encryptionEnforced;
+ enableUnknownUserActivation = builder.enableUnknownUserActivation;
+ enableForegroundCriteriaFetch = builder.enableForegroundCriteriaFetch;
enableEmbeddedMessaging = builder.enableEmbeddedMessaging;
keychainEncryption = builder.keychainEncryption;
+ eventThresholdLimit = builder.eventThresholdLimit;
+ identityResolution = builder.identityResolution;
+ iterableUnknownUserHandler = builder.iterableUnknownUserHandler;
decryptionFailureHandler = builder.decryptionFailureHandler;
mobileFrameworkInfo = builder.mobileFrameworkInfo;
}
@@ -147,13 +182,29 @@ public static class Builder {
private String[] allowedProtocols = new String[0];
private IterableDataRegion dataRegion = IterableDataRegion.US;
private boolean useInMemoryStorageForInApps = false;
- private boolean enableEmbeddedMessaging = false;
private boolean keychainEncryption = true;
- private IterableDecryptionFailureHandler decryptionFailureHandler;
private IterableAPIMobileFrameworkInfo mobileFrameworkInfo;
+ private IterableDecryptionFailureHandler decryptionFailureHandler;
+ private boolean encryptionEnforced = false;
+ private boolean enableUnknownUserActivation = false;
+ private boolean enableForegroundCriteriaFetch = true;
+ private boolean enableEmbeddedMessaging = false;
+ private int eventThresholdLimit = 100;
+ private IterableIdentityResolution identityResolution = new IterableIdentityResolution();
+ private IterableUnknownUserHandler iterableUnknownUserHandler;
public Builder() {}
+ /**
+ * Set a custom unknown user handler which is called when an unknown user is generated
+ * @param iterableUnknownUserHandler Custom unknown user handler provided by the app
+ */
+ @NonNull
+ public Builder setUnknownUserHandler(@NonNull IterableUnknownUserHandler iterableUnknownUserHandler) {
+ this.iterableUnknownUserHandler = iterableUnknownUserHandler;
+ return this;
+ }
+
/**
* Push integration name - used for token registration
* Make sure the name of this integration matches the one set up in Iterable console
@@ -302,6 +353,35 @@ public Builder setUseInMemoryStorageForInApps(boolean useInMemoryStorageForInApp
return this;
}
+ /**
+ * Set whether the SDK should track events for unknown users. Set this to `true`
+ * if you want to track all events when users are not logged into the application.
+ * @param enableUnknownUserActivation `true` will track events for unknown users.
+ */
+ public Builder setEnableUnknownUserActivation(boolean enableUnknownUserActivation) {
+ this.enableUnknownUserActivation = enableUnknownUserActivation;
+ return this;
+ }
+
+ /**
+ * Set whether the SDK should disable criteria fetching on foregrounding. Set this to `false`
+ * if you want criteria to only be fetched on app launch.
+ * @param enableForegroundCriteriaFetch `true` will fetch criteria only on app launch.
+ */
+ public Builder setEnableForegroundCriteriaFetch(boolean enableForegroundCriteriaFetch) {
+ this.enableForegroundCriteriaFetch = enableForegroundCriteriaFetch;
+ return this;
+ }
+
+ /**
+ * Set the number of unknown user events stored in local storage
+ * @param eventThresholdLimit the number of unknown user events stored in local storage
+ */
+ public Builder setEventThresholdLimit(int eventThresholdLimit) {
+ this.eventThresholdLimit = eventThresholdLimit;
+ return this;
+ }
+
/**
* Allows for fetching embedded messages.
* @param enableEmbeddedMessaging `true` will allow automatically fetching embedded messaging.
@@ -322,6 +402,18 @@ public Builder setKeychainEncryption(boolean keychainEncryption) {
return this;
}
+ /**
+ * Set whether the SDK should replay events from local storage to the logged in profile
+ * and set whether the SDK should merge the generated unknown user profile and the logged in profile.
+ * This can be overwritten by a parameter passed into setEmail or setUserId.
+ * @param identityResolution identify resolution object
+ * @return
+ */
+ public Builder setIdentityResolution(IterableIdentityResolution identityResolution) {
+ this.identityResolution = identityResolution;
+ return this;
+ }
+
/**
* Set a handler for decryption failures that can be used to handle data recovery
* @param handler Decryption failure handler provided by the app
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java
index 9eadb6fef..85c4b7066 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java
@@ -16,6 +16,7 @@ public final class IterableConstants {
//API Fields
public static final String HEADER_API_KEY = "Api-Key";
+ public static final String HTTP_STATUS_CODE = "httpStatusCode";
public static final String HEADER_SDK_PLATFORM = "SDK-Platform";
public static final String HEADER_SDK_VERSION = "SDK-Version";
public static final String HEADER_SDK_AUTHORIZATION = "Authorization";
@@ -56,6 +57,10 @@ public final class IterableConstants {
public static final String KEY_EMBEDDED_SESSION_ID = "id";
public static final String KEY_OFFLINE_MODE = "offlineMode";
public static final String KEY_FIRETV = "FireTV";
+ public static final String KEY_CREATE_NEW_FIELDS = "createNewFields";
+ public static final String KEY_IS_USER_KNOWN = "isUserKnown";
+ public static final String KEY_CONSENT_TIMESTAMP = "consentTimestamp";
+ public static final String KEY_UNKNOWN_SESSION_CONTEXT = "unknownSessionContext";
//API Endpoint Key Constants
public static final String ENDPOINT_DISABLE_DEVICE = "users/disableDevice";
@@ -80,6 +85,12 @@ public final class IterableConstants {
public static final String ENDPOINT_TRACK_EMBEDDED_RECEIVED = "embedded-messaging/events/received";
public static final String ENDPOINT_TRACK_EMBEDDED_CLICK = "embedded-messaging/events/click";
public static final String ENDPOINT_TRACK_EMBEDDED_SESSION = "embedded-messaging/events/session";
+ public static final String ENDPOINT_GET_USER_BY_USERID = "users/byUserId";
+ public static final String ENDPOINT_GET_USER_BY_EMAIL = "users/getByEmail";
+ public static final String ENDPOINT_MERGE_USER = "users/merge";
+ public static final String ENDPOINT_TRACK_CONSENT = "unknownuser/consent";
+ public static final String ENDPOINT_CRITERIA_LIST = "unknownuser/list";
+ public static final String ENDPOINT_TRACK_UNKNOWN_SESSION = "unknownuser/events/session";
public static final String PUSH_APP_ID = "IterableAppId";
public static final String PUSH_GCM_PROJECT_NUMBER = "GCMProjectNumber";
@@ -107,6 +118,7 @@ public final class IterableConstants {
public static final String SHARED_PREFS_FILE = "com.iterable.iterableapi";
public static final String SHARED_PREFS_EMAIL_KEY = "itbl_email";
public static final String SHARED_PREFS_USERID_KEY = "itbl_userid";
+ public static final String SHARED_PREFS_USERIDUNKNOWN_KEY = "itbl_userid_unknown";
public static final String SHARED_PREFS_DEVICEID_KEY = "itbl_deviceid";
public static final String SHARED_PREFS_AUTH_TOKEN_KEY = "itbl_authtoken";
public static final String SHARED_PREFS_EXPIRATION_SUFFIX = "_expiration";
@@ -118,7 +130,20 @@ public final class IterableConstants {
public static final String SHARED_PREFS_FCM_MIGRATION_DONE_KEY = "itbl_fcm_migration_done";
public static final String SHARED_PREFS_SAVED_CONFIGURATION = "itbl_saved_configuration";
public static final String SHARED_PREFS_OFFLINE_MODE_KEY = "itbl_offline_mode";
+ public static final String SHARED_PREFS_EVENT_LIST_KEY = "itbl_event_list";
+ public static final String SHARED_PREFS_USER_UPDATE_OBJECT_KEY = "itbl_user_update_object";
+ public static final String SHARED_PREFS_UNKNOWN_SESSIONS = "itbl_unknown_sessions";
+ public static final String SHARED_PREFS_SESSION_NO = "totalUnknownSessionCount";
+ public static final String SHARED_PREFS_LAST_SESSION = "lastUnknownSession";
+ public static final String SHARED_PREFS_FIRST_SESSION = "firstUnknownSession";
+ public static final String SHARED_PREFS_EVENT_TYPE = "eventType";
+ public static final String SHARED_PREFS_CRITERIA = "criteria";
+ public static final String SHARED_PREFS_CRITERIA_ID = "matchedCriteriaId";
+ public static final String SHARED_PREFS_PUSH_OPT_IN = "mobilePushOptIn";
+ public static final String SHARED_PREFS_VISITOR_USAGE_TRACKED = "itbl_visitor_usage_track";
+ public static final String SHARED_PREFS_VISITOR_USAGE_TRACKED_TIME = "itbl_visitor_usage_track_time";
public static final String SHARED_PREFS_DEVICE_NOTIFICATIONS_ENABLED = "itbl_notifications_enabled";
+ public static final String SHARED_PREFS_CONSENT_LOGGED = "itbl_consent_logged";
//Action buttons
public static final String ITBL_BUTTON_IDENTIFIER = "identifier";
@@ -161,6 +186,7 @@ public final class IterableConstants {
public static final String REQUEST_CODE = "requestCode";
public static final String ACTION_IDENTIFIER = "actionIdentifier";
public static final String USER_INPUT = "userInput";
+ public static final String DATA_REPLACE = "dataReplace";
//Firebase
public static final String FIREBASE_SENDER_ID = "gcm_defaultSenderId";
@@ -300,4 +326,38 @@ public final class IterableConstants {
public static final String NO_MESSAGES_TITLE = "noMessagesTitle";
public static final String NO_MESSAGES_BODY = "noMessagesBody";
+
+ // Criteria constants
+ public static final String CRITERIA_SETS = "criteriaSets";
+ public static final String SEARCH_QUERIES = "searchQueries";
+ public static final String SEARCH_QUERY = "searchQuery";
+ public static final String CRITERIA_ID = "criteriaId";
+ public static final String COMBINATOR = "combinator";
+ public static final String SEARCH_COMBO = "searchCombo";
+ public static final String FIELD = "field";
+ public static final String VALUE = "value";
+ public static final String VALUES = "values";
+ public static final String DATA_TYPE = "dataType";
+ public static final String COMPARATOR_TYPE = "comparatorType";
+ public static final String UPDATECART_ITEM_PREFIX = "updateCart.updatedShoppingCartItems.";
+ public static final String PURCHASE_ITEM = "shoppingCartItems";
+ public static final String PURCHASE_ITEM_PREFIX = PURCHASE_ITEM + ".";
+ public static final String MIN_MATCH = "minMatch";
+ public static final Integer CRITERIA_FETCHING_COOLDOWN = 120000;
+
+ //Tracking types
+ public static final String TRACK_EVENT = "customEvent";
+ public static final String TRACK_PURCHASE = "purchase";
+ public static final String TRACK_UPDATE_CART = "cartUpdate";
+ public static final String UPDATE_CART = "updateCart";
+ public static final String TRACK_TOKEN_REGISTRATION = "tokenRegistration";
+ public static final String UPDATE_USER = "user";
+ public static final String SOURCE_EMAIL = "sourceEmail";
+ public static final String SOURCE_USER_ID = "sourceUserId";
+ public static final String DESTINATION_EMAIL = "destinationEmail";
+ public static final String DESTINATION_USER_ID = "destinationUserId";
+
+ // Merge user constants
+ public static final String MERGE_SUCCESSFUL = "merge_successful";
+ public static final String MERGE_NOTREQUIRED = "merge_notrequired";
}
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableIdentityResolution.kt b/iterableapi/src/main/java/com/iterable/iterableapi/IterableIdentityResolution.kt
new file mode 100644
index 000000000..a00be53fa
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableIdentityResolution.kt
@@ -0,0 +1,6 @@
+package com.iterable.iterableapi
+
+data class IterableIdentityResolution (
+ val replayOnVisitorToKnown: Boolean = true,
+ val mergeOnUnknownToKnown: Boolean = true
+)
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableKeychain.kt b/iterableapi/src/main/java/com/iterable/iterableapi/IterableKeychain.kt
index 875ce5e7a..1a8235b09 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableKeychain.kt
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableKeychain.kt
@@ -11,6 +11,7 @@ class IterableKeychain {
private const val TAG = "IterableKeychain"
const val KEY_EMAIL = "iterable-email"
const val KEY_USER_ID = "iterable-user-id"
+ const val KEY_UNKNOWN_USER_ID = "iterable-unknown-user-id"
const val KEY_AUTH_TOKEN = "iterable-auth-token"
private const val PLAINTEXT_SUFFIX = "_plaintext"
private const val CRYPTO_OPERATION_TIMEOUT_MS = 500L
@@ -80,6 +81,7 @@ class IterableKeychain {
sharedPrefs.edit()
.remove(KEY_EMAIL)
.remove(KEY_USER_ID)
+ .remove(KEY_UNKNOWN_USER_ID)
.remove(KEY_AUTH_TOKEN)
.putBoolean(KEY_ENCRYPTION_ENABLED, false)
.apply()
@@ -159,4 +161,7 @@ class IterableKeychain {
fun getAuthToken() = secureGet(KEY_AUTH_TOKEN)
fun saveAuthToken(authToken: String?) = secureSave(KEY_AUTH_TOKEN, authToken)
-}
\ No newline at end of file
+
+ fun getUserIdUnknown() = secureGet(KEY_UNKNOWN_USER_ID)
+ fun saveUserIdUnknown(userIdUnknown: String?) = secureSave(KEY_UNKNOWN_USER_ID, userIdUnknown)
+}
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java
index 9fecbc807..f052da780 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableRequestTask.java
@@ -381,7 +381,13 @@ private void handleSuccessResponse(IterableApiResponse response) {
private void handleErrorResponse(IterableApiResponse response) {
if (iterableApiRequest.failureCallback != null) {
- iterableApiRequest.failureCallback.onFailure(response.errorMessage, response.responseJson);
+ JSONObject responseJson = response.responseJson;
+ if (responseJson != null) {
+ try {
+ responseJson.put(IterableConstants.HTTP_STATUS_CODE, response.responseCode);
+ } catch (JSONException e) {}
+ }
+ iterableApiRequest.failureCallback.onFailure(response.errorMessage, responseJson);
}
}
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableUnknownUserHandler.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableUnknownUserHandler.java
new file mode 100644
index 000000000..893345a25
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableUnknownUserHandler.java
@@ -0,0 +1,5 @@
+package com.iterable.iterableapi;
+
+public interface IterableUnknownUserHandler {
+ void onUnknownUserCreated(String userId);
+}
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/MergeResultCallback.java b/iterableapi/src/main/java/com/iterable/iterableapi/MergeResultCallback.java
new file mode 100644
index 000000000..c3f1e731f
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/MergeResultCallback.java
@@ -0,0 +1,5 @@
+package com.iterable.iterableapi;
+
+public interface MergeResultCallback {
+ void onResult(String mergeResult, String error);
+}
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/OnlineRequestProcessor.java b/iterableapi/src/main/java/com/iterable/iterableapi/OnlineRequestProcessor.java
index 9b3cc2247..013f7f0ad 100644
--- a/iterableapi/src/main/java/com/iterable/iterableapi/OnlineRequestProcessor.java
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/OnlineRequestProcessor.java
@@ -40,7 +40,13 @@ public void onLogout(Context context) {
JSONObject addCreatedAtToJson(JSONObject jsonObject) {
try {
- jsonObject.put(IterableConstants.KEY_CREATED_AT, new Date().getTime() / 1000);
+ long createdAt;
+ if (jsonObject.has(IterableConstants.KEY_CREATED_AT)) {
+ createdAt = Long.parseLong(jsonObject.getString(IterableConstants.KEY_CREATED_AT));
+ } else {
+ createdAt = new Date().getTime() / 1000;
+ }
+ jsonObject.put(IterableConstants.KEY_CREATED_AT, createdAt);
} catch (JSONException e) {
IterableLogger.e(TAG, "Could not add createdAt timestamp to json object");
}
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/UnknownUserManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/UnknownUserManager.java
new file mode 100644
index 000000000..c977e048f
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/UnknownUserManager.java
@@ -0,0 +1,510 @@
+package com.iterable.iterableapi;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.app.NotificationManagerCompat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import com.iterable.iterableapi.util.CriteriaCompletionChecker;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+public class UnknownUserManager implements IterableActivityMonitor.AppStateCallback {
+
+ private static final String TAG = "UnknownUserManager";
+ private IterableApi iterableApi = IterableApi.sharedInstance;
+ private final IterableActivityMonitor activityMonitor;
+ long lastCriteriaFetch = 0;
+ boolean isCriteriaMatched = false;
+
+ UnknownUserManager(IterableApi iterableApi) {
+ this(iterableApi,
+ IterableActivityMonitor.getInstance());
+ }
+
+ @VisibleForTesting
+ UnknownUserManager(IterableApi iterableApi,
+ IterableActivityMonitor activityMonitor) {
+ this.iterableApi = iterableApi;
+ this.activityMonitor = activityMonitor;
+ this.activityMonitor.addCallback(this);
+ }
+
+ void updateUnknownSession() {
+ IterableLogger.v(TAG, "updateUnknownSession");
+
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ String previousData = sharedPref.getString(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, "");
+
+ try {
+ int sessionNo = 0;
+ String firstSessionDate = "";
+
+ //If previous session data exists, get previous session number and first session date
+ if (!previousData.isEmpty()) {
+ JSONObject previousDataJson = new JSONObject(previousData);
+ JSONObject sessionObject = previousDataJson.getJSONObject(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS);
+ sessionNo = sessionObject.getInt(IterableConstants.SHARED_PREFS_SESSION_NO);
+ firstSessionDate = sessionObject.getString(IterableConstants.SHARED_PREFS_FIRST_SESSION);
+ }
+
+ //create new session data object and save it to local storage
+ JSONObject newDataObject = createNewSessionData(sessionNo, firstSessionDate);
+ saveUnknownSessionData(sharedPref, newDataObject);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private JSONObject createNewSessionData(int sessionNo, String firstSessionDate) throws JSONException {
+ JSONObject newDataObject = new JSONObject();
+ newDataObject.put(IterableConstants.SHARED_PREFS_SESSION_NO, sessionNo + 1);
+ newDataObject.put(IterableConstants.SHARED_PREFS_LAST_SESSION, IterableUtil.currentTimeMillis());
+
+ if (firstSessionDate.isEmpty()) {
+ newDataObject.put(IterableConstants.SHARED_PREFS_FIRST_SESSION, IterableUtil.currentTimeMillis());
+ } else {
+ newDataObject.put(IterableConstants.SHARED_PREFS_FIRST_SESSION, firstSessionDate);
+ }
+
+ return newDataObject;
+ }
+
+ private void saveUnknownSessionData(SharedPreferences sharedPref, JSONObject newDataObject) throws JSONException {
+ JSONObject unknownSessionData = new JSONObject();
+ unknownSessionData.put(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, newDataObject);
+
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, unknownSessionData.toString());
+ editor.apply();
+ }
+
+ void trackUnknownEvent(String eventName, JSONObject dataFields) {
+ IterableLogger.v(TAG, "trackUnknownEvent");
+
+ try {
+ JSONObject newDataObject = new JSONObject();
+ newDataObject.put(IterableConstants.KEY_EVENT_NAME, eventName);
+ newDataObject.put(IterableConstants.KEY_CREATED_AT, IterableUtil.currentTimeMillis());
+ newDataObject.put(IterableConstants.KEY_DATA_FIELDS, dataFields);
+ newDataObject.put(IterableConstants.KEY_CREATE_NEW_FIELDS, true);
+ newDataObject.put(IterableConstants.SHARED_PREFS_EVENT_TYPE, IterableConstants.TRACK_EVENT);
+ storeEventListToLocalStorage(newDataObject);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void trackUnknownUpdateUser(JSONObject dataFields) {
+ IterableLogger.v(TAG, "updateUnknownUser");
+ try {
+ JSONObject newDataObject = new JSONObject();
+ newDataObject.put(IterableConstants.KEY_DATA_FIELDS, dataFields);
+ newDataObject.put(IterableConstants.SHARED_PREFS_EVENT_TYPE, IterableConstants.UPDATE_USER);
+ storeUserUpdateToLocalStorage(newDataObject);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void trackUnknownTokenRegistration(String token) {
+ IterableLogger.v(TAG, "trackUnknownTokenRegistration");
+ try {
+ JSONObject newDataObject = new JSONObject();
+ newDataObject.put(IterableConstants.KEY_TOKEN, token);
+ newDataObject.put(IterableConstants.SHARED_PREFS_EVENT_TYPE, IterableConstants.TRACK_TOKEN_REGISTRATION);
+ storeEventListToLocalStorage(newDataObject);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void trackUnknownPurchaseEvent(double total, @NonNull List items, @Nullable JSONObject dataFields) {
+ IterableLogger.v(TAG, "trackUnknownPurchaseEvent");
+ try {
+ JSONObject newDataObject = new JSONObject();
+ Gson gson = new GsonBuilder().create();
+
+ newDataObject.put(IterableConstants.KEY_ITEMS, gson.toJsonTree(items).getAsJsonArray().toString());
+ newDataObject.put(IterableConstants.KEY_CREATED_AT, IterableUtil.currentTimeMillis());
+ newDataObject.put(IterableConstants.KEY_DATA_FIELDS, dataFields);
+ newDataObject.put(IterableConstants.KEY_TOTAL, total);
+ newDataObject.put(IterableConstants.SHARED_PREFS_EVENT_TYPE, IterableConstants.TRACK_PURCHASE);
+ storeEventListToLocalStorage(newDataObject);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void trackUnknownUpdateCart(@NonNull List items) {
+ IterableLogger.v(TAG, "trackUnknownUpdateCart");
+
+ try {
+ Gson gson = new GsonBuilder().create();
+ JSONObject newDataObject = new JSONObject();
+ newDataObject.put(IterableConstants.KEY_ITEMS, gson.toJsonTree(items).getAsJsonArray().toString());
+ newDataObject.put(IterableConstants.SHARED_PREFS_EVENT_TYPE, IterableConstants.TRACK_UPDATE_CART);
+ newDataObject.put(IterableConstants.KEY_CREATED_AT, IterableUtil.currentTimeMillis());
+ storeEventListToLocalStorage(newDataObject);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ void getCriteria() {
+ lastCriteriaFetch = System.currentTimeMillis();
+
+ iterableApi.apiClient.getCriteriaList(data -> {
+ if (data != null) {
+ try {
+ JSONObject mockDataObject = new JSONObject(data);
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_CRITERIA, mockDataObject.toString());
+ editor.apply();
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ private String checkCriteriaCompletion() {
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ String criteriaData = sharedPref.getString(IterableConstants.SHARED_PREFS_CRITERIA, "");
+
+ JSONArray localStoredEventListAndUserUpdates = getEventListFromLocalStorage();
+ JSONObject localStoredUserUpdateObj = getUserUpdateObjFromLocalStorage();
+
+ localStoredEventListAndUserUpdates.put(localStoredUserUpdateObj);
+
+ try {
+ if (!criteriaData.isEmpty() && localStoredEventListAndUserUpdates.length() > 0) {
+ CriteriaCompletionChecker checker = new CriteriaCompletionChecker();
+ return checker.getMatchedCriteria(criteriaData, localStoredEventListAndUserUpdates);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ private void createUnknownUser(String criteriaId) {
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ updateUnknownSession();
+
+ //get session data
+ String userData = sharedPref.getString(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, "");
+
+ //generate unknown user id
+ String userId = UUID.randomUUID().toString();
+
+ try {
+ if (!userData.isEmpty()) {
+ JSONObject updateUserObj = getUserUpdateObjFromLocalStorage();
+ JSONObject updateUserDataFields = null;
+ if (updateUserObj.has(IterableConstants.KEY_DATA_FIELDS)) {
+ updateUserDataFields = updateUserObj.getJSONObject(IterableConstants.KEY_DATA_FIELDS);
+ }
+ JSONObject userSessionDataJson = new JSONObject(userData);
+ JSONObject userDataJson = userSessionDataJson.getJSONObject(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS);
+
+ //update user data
+ if (!getPushStatus().isEmpty()) {
+ userDataJson.put(IterableConstants.SHARED_PREFS_PUSH_OPT_IN, getPushStatus());
+ }
+ userDataJson.put(IterableConstants.SHARED_PREFS_CRITERIA_ID, Integer.valueOf(criteriaId));
+
+ //track unknown user session with new user
+ iterableApi.apiClient.trackUnknownUserSession(IterableUtil.currentTimeMillis(), userId, userDataJson, updateUserDataFields, data -> {
+ // success handler
+ if (IterableApi.getInstance().config.iterableUnknownUserHandler != null) {
+ IterableApi.getInstance().config.iterableUnknownUserHandler.onUnknownUserCreated(userId);
+ }
+ IterableApi.getInstance().setUnknownUser(userId);
+ }, (reason, data) -> handleTrackFailure(data));
+ }
+
+ } catch (JSONException e) {
+ isCriteriaMatched = false;
+ e.printStackTrace();
+ }
+ }
+
+ private void handleTrackFailure(JSONObject data) {
+ isCriteriaMatched = false;
+ if (data != null && data.has(IterableConstants.HTTP_STATUS_CODE)) {
+ try {
+ int statusCode = (int) data.get(IterableConstants.HTTP_STATUS_CODE);
+ if (statusCode == 409) {
+ getCriteria(); // refetch the criteria
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ void syncEventsAndUserUpdate() {
+ JSONArray trackEventList = getEventListFromLocalStorage();
+ JSONObject updateUserObj = getUserUpdateObjFromLocalStorage();
+ Gson gson = new GsonBuilder().create();
+
+ if (trackEventList.length() == 0 && !updateUserObj.has(IterableConstants.KEY_DATA_FIELDS)) return;
+
+ for (int i = 0; i < trackEventList.length(); i++) {
+ try {
+ JSONObject event = trackEventList.getJSONObject(i);
+ String eventType = event.getString(IterableConstants.SHARED_PREFS_EVENT_TYPE);
+ switch (eventType) {
+ case IterableConstants.TRACK_EVENT: {
+ handleTrackEvent(event);
+ break;
+ }
+ case IterableConstants.TRACK_PURCHASE: {
+ handleTrackPurchase(event, gson);
+ break;
+ }
+ case IterableConstants.TRACK_UPDATE_CART: {
+ handleUpdateCart(event, gson);
+ break;
+ }
+ default:
+ break;
+ }
+ } catch (JSONException e) {
+ IterableLogger.d(TAG, "Event Sync Failure");
+ }
+ }
+
+ try {
+ handleUpdateUser(updateUserObj);
+ } catch (JSONException e) {
+ IterableLogger.d(TAG, "Handle User Update Failure");
+ }
+ }
+
+ private void handleTrackEvent(JSONObject event) throws JSONException {
+ String createdAt = getStringValue(event);
+ JSONObject dataFields = getDataFields(event);
+ iterableApi.apiClient.track(event.getString(IterableConstants.KEY_EVENT_NAME), 0, 0, dataFields, createdAt);
+ }
+
+ private void handleTrackPurchase(JSONObject event, Gson gson) throws JSONException {
+ Type listType = new TypeToken>() { }.getType();
+ List list = gson.fromJson(event.getString(IterableConstants.KEY_ITEMS), listType);
+
+ long createdAt = getLongValue(event);
+ JSONObject dataFields = getDataFields(event);
+ iterableApi.apiClient.trackPurchase(event.getDouble(IterableConstants.KEY_TOTAL), list, dataFields, createdAt);
+ }
+
+ private void handleUpdateCart(JSONObject event, Gson gson) throws JSONException {
+ Type listType = new TypeToken>() { }.getType();
+ List list = gson.fromJson(event.getString(IterableConstants.KEY_ITEMS), listType);
+
+ long createdAt = getLongValue(event);
+ iterableApi.apiClient.updateCart(list, createdAt);
+ }
+
+ private void handleUpdateUser(JSONObject event) throws JSONException {
+ iterableApi.apiClient.updateUser(event.getJSONObject(IterableConstants.KEY_DATA_FIELDS), false);
+ }
+
+ private String getStringValue(JSONObject event) throws JSONException {
+ return event.has(IterableConstants.KEY_CREATED_AT) ? event.getString(IterableConstants.KEY_CREATED_AT) : "";
+ }
+
+ private long getLongValue(JSONObject event) throws JSONException {
+ return event.has(IterableConstants.KEY_CREATED_AT) ? Long.parseLong(event.getString(IterableConstants.KEY_CREATED_AT)) : 0L;
+ }
+ private JSONObject getDataFields(JSONObject event) throws JSONException {
+ return event.has(IterableConstants.KEY_DATA_FIELDS) ? new JSONObject(event.getString(IterableConstants.KEY_DATA_FIELDS)) : null;
+ }
+
+ public void clearVisitorEventsAndUserData() {
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, "");
+ editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ editor.putString(IterableConstants.SHARED_PREFS_USER_UPDATE_OBJECT_KEY, "");
+ editor.apply();
+ }
+
+ private void storeEventListToLocalStorage(JSONObject newDataObject) {
+ if (!iterableApi.getVisitorUsageTracked()) {
+ return;
+ }
+ JSONArray eventList = getEventListFromLocalStorage();
+
+ eventList.put(newDataObject);
+
+ eventList = enforceEventThresholdLimit(eventList);
+ saveEventListToLocalStorage(eventList);
+
+ String criteriaId = checkCriteriaCompletion();
+ Log.i("TEST_USER", "criteriaId::" + String.valueOf(criteriaId));
+
+ if (criteriaId != null && !isCriteriaMatched) {
+ setCriteriaMatched(true);
+ createUnknownUser(criteriaId);
+ }
+ Log.i("criteriaId::", String.valueOf(criteriaId != null));
+ }
+
+ void setCriteriaMatched(boolean isCriteriaMatched) {
+ this.isCriteriaMatched = isCriteriaMatched;
+ }
+
+ private void storeUserUpdateToLocalStorage(JSONObject newDataObject) throws JSONException {
+ if (!iterableApi.getVisitorUsageTracked()) {
+ return;
+ }
+
+ JSONObject userUpdateObject = getUserUpdateObjFromLocalStorage();
+ mergeUpdateUserObjects(userUpdateObject, newDataObject);
+
+ saveUserUpdateObjectToLocalStorage(userUpdateObject);
+
+ String criteriaId = checkCriteriaCompletion();
+ Log.i("TEST_USER", "criteriaId::" + String.valueOf(criteriaId));
+
+ if (criteriaId != null) {
+ createUnknownUser(criteriaId);
+ }
+ Log.i("criteriaId::", String.valueOf(criteriaId != null));
+ }
+
+ private void mergeUpdateUserObjects(JSONObject target, JSONObject source) throws JSONException {
+ Iterator keys = source.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object value = source.get(key);
+ // if value is an object, recurse
+ if (value instanceof JSONObject && target.has(key)) {
+ mergeUpdateUserObjects(target.getJSONObject(key), (JSONObject) value);
+ } else {
+ // If the key doesn't exist in the target, just add it
+ target.put(key, value);
+ }
+ }
+ }
+
+ private JSONArray enforceEventThresholdLimit(JSONArray eventDataArray) {
+ int lengthOfData = eventDataArray.length();
+ int eventThresholdLimit = iterableApi.config.eventThresholdLimit;
+
+ if (lengthOfData > eventThresholdLimit) {
+ int difference = lengthOfData - eventThresholdLimit;
+ ArrayList eventListData = new ArrayList<>();
+ for (int i = difference; i < eventDataArray.length(); i++) {
+ try {
+ eventListData.add(eventDataArray.getJSONObject(i));
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return new JSONArray(eventListData);
+ }
+ return eventDataArray;
+ }
+
+ private void saveEventListToLocalStorage(JSONArray eventDataArray) {
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, eventDataArray.toString());
+ editor.apply();
+ }
+
+ private void saveUserUpdateObjectToLocalStorage(JSONObject userUpdateObject) {
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_USER_UPDATE_OBJECT_KEY, userUpdateObject.toString());
+ editor.apply();
+ }
+
+ private JSONArray getEventListFromLocalStorage() {
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ String eventListJson = sharedPref.getString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ JSONArray eventListArray = new JSONArray();
+ try {
+ if (!eventListJson.isEmpty()) {
+ eventListArray = new JSONArray(eventListJson);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return eventListArray;
+ }
+
+ private JSONObject getUserUpdateObjFromLocalStorage() {
+ SharedPreferences sharedPref = IterableApi.getInstance().getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ String userUpdateJson = sharedPref.getString(IterableConstants.SHARED_PREFS_USER_UPDATE_OBJECT_KEY, "");
+ JSONObject userUpdateObject = new JSONObject();
+ try {
+ if (!userUpdateJson.isEmpty()) {
+ userUpdateObject = new JSONObject(userUpdateJson);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return userUpdateObject;
+ }
+
+ private String getPushStatus() {
+ NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(IterableApi.getInstance().getMainActivityContext());
+ if (notificationManagerCompat.areNotificationsEnabled()) {
+ ApplicationInfo applicationInfo = IterableApi.getInstance().getMainActivityContext().getApplicationInfo();
+ int stringId = applicationInfo.labelRes;
+ return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : IterableApi.getInstance().getMainActivityContext().getString(stringId);
+ } else {
+ return "";
+ }
+ }
+
+ @Override
+ public void onSwitchToForeground() {
+ long currentTime = System.currentTimeMillis();
+
+ // fetching unknown user criteria on foregrounding
+ if (!iterableApi.checkSDKInitialization()
+ && iterableApi._userIdUnknown == null
+ && iterableApi.config.enableUnknownUserActivation
+ && iterableApi.getVisitorUsageTracked()
+ && iterableApi.config.enableForegroundCriteriaFetch
+ && currentTime - lastCriteriaFetch >= IterableConstants.CRITERIA_FETCHING_COOLDOWN) {
+
+ lastCriteriaFetch = currentTime;
+ this.getCriteria();
+ IterableLogger.d(TAG, "Fetching unknown user criteria - Foreground");
+ }
+ }
+
+ @Override
+ public void onSwitchToBackground() {
+
+ }
+}
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/UnknownUserMerge.java b/iterableapi/src/main/java/com/iterable/iterableapi/UnknownUserMerge.java
new file mode 100644
index 000000000..4a63e3222
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/UnknownUserMerge.java
@@ -0,0 +1,26 @@
+package com.iterable.iterableapi;
+
+public class UnknownUserMerge {
+ private static final String TAG = "UnknownUserMerge";
+
+ void tryMergeUser(IterableApiClient apiClient, String unknownUserId, String destinationUser, boolean isEmail, boolean merge, MergeResultCallback callback) {
+ IterableLogger.v(TAG, "tryMergeUser");
+ if (unknownUserId != null && merge) {
+ String destinationEmail = isEmail ? destinationUser : null;
+ String destinationUserId = isEmail ? null : destinationUser;
+ apiClient.mergeUser(null, unknownUserId, destinationEmail, destinationUserId, data -> {
+ if (callback != null) {
+ callback.onResult(IterableConstants.MERGE_SUCCESSFUL, null); // Notify success
+ }
+ }, (reason, data) -> {
+ if (callback != null) {
+ callback.onResult(null, reason); // Notify failure
+ }
+ });
+ } else {
+ if (callback != null) {
+ callback.onResult(IterableConstants.MERGE_NOTREQUIRED, null); // Return true if inputs are null as per original logic
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/ddl/Criteria.java b/iterableapi/src/main/java/com/iterable/iterableapi/ddl/Criteria.java
new file mode 100644
index 000000000..0ad9ffa5e
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/ddl/Criteria.java
@@ -0,0 +1,55 @@
+package com.iterable.iterableapi.ddl;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Criteria {
+ public final String criteriaId;
+ public final List criteriaList;
+
+ public Criteria(String criteriaId, List criteriaList) {
+ this.criteriaId = criteriaId;
+ this.criteriaList = criteriaList;
+ }
+
+ public static Criteria fromJSONObject(JSONObject json) throws JSONException {
+
+ List criteriaList = new ArrayList<>();
+ JSONArray jsonArray = json.getJSONArray("criteriaList");
+ for (int i = 0; i < jsonArray.length(); i++) {
+ criteriaList.add(CriteriaList.fromJSONObject(jsonArray.getJSONObject(i)));
+ }
+ return new Criteria(
+ json.getString("criteriaId"),
+ criteriaList
+ );
+ }
+
+ public static class CriteriaList {
+ public final String criteriaType;
+ public final String comparator;
+ public final String name;
+ public final int aggregateCount;
+
+ public CriteriaList(String criteriaType, String comparator, String name, int aggregateCount) {
+ this.criteriaType = criteriaType;
+ this.comparator = comparator;
+ this.name = name;
+ this.aggregateCount = aggregateCount;
+ }
+
+ static CriteriaList fromJSONObject(JSONObject json) throws JSONException {
+ return new CriteriaList(
+ json.getString("criteriaType"),
+ json.getString("comparator"),
+ json.getString("name"),
+ json.getInt("aggregateCount")
+ );
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java b/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java
new file mode 100644
index 000000000..49057a9a0
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java
@@ -0,0 +1,824 @@
+package com.iterable.iterableapi.util;
+
+import androidx.annotation.NonNull;
+import com.iterable.iterableapi.IterableConstants;
+import com.iterable.iterableapi.IterableLogger;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.Map;
+
+public class CriteriaCompletionChecker {
+
+ private JSONArray localStoredEventList;
+
+ public String getMatchedCriteria(String criteriaData, JSONArray localStoredEventList) {
+ this.localStoredEventList = localStoredEventList;
+ String criteriaId = null;
+
+ try {
+ JSONObject json = new JSONObject(criteriaData);
+ if (json.has(IterableConstants.CRITERIA_SETS)) {
+ JSONArray criteriaList = json.getJSONArray(IterableConstants.CRITERIA_SETS);
+ criteriaId = findMatchedCriteria(criteriaList);
+ }
+ } catch (JSONException e) {
+ handleJSONException(e);
+ }
+
+ return criteriaId;
+ }
+
+ private String findMatchedCriteria(JSONArray criteriaList) {
+ String criteriaId = null;
+ JSONArray eventsToProcess = prepareEventsToProcess();
+
+ for (int i = 0; i < criteriaList.length(); i++) {
+ try {
+ JSONObject criteria = criteriaList.getJSONObject(i);
+ if (criteria.has(IterableConstants.SEARCH_QUERY) && criteria.has(IterableConstants.CRITERIA_ID)) {
+ JSONObject searchQuery = criteria.getJSONObject(IterableConstants.SEARCH_QUERY);
+ String currentCriteriaId = criteria.getString(IterableConstants.CRITERIA_ID);
+ boolean result = evaluateTree(searchQuery, eventsToProcess);
+ if (result) {
+ criteriaId = currentCriteriaId;
+ break;
+ }
+ }
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return criteriaId;
+ }
+
+ private JSONArray prepareEventsToProcess() {
+ JSONArray eventsToProcess = getEventsWithCartItems();
+ JSONArray nonPurchaseEvents = getNonCartEvents();
+
+ for (int i = 0; i < nonPurchaseEvents.length(); i++) {
+ try {
+ eventsToProcess.put(nonPurchaseEvents.getJSONObject(i));
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return eventsToProcess;
+ }
+
+ private JSONArray getEventsWithCartItems() {
+ JSONArray processedEvents = new JSONArray();
+ try {
+ for (int i = 0; i < localStoredEventList.length(); i++) {
+ JSONObject localEventData = localStoredEventList.getJSONObject(i);
+ if (localEventData.has(IterableConstants.SHARED_PREFS_EVENT_TYPE) && (
+ localEventData.get(IterableConstants.SHARED_PREFS_EVENT_TYPE).equals(IterableConstants.TRACK_PURCHASE))) {
+ JSONObject updatedItem = new JSONObject();
+
+ if (localEventData.has(IterableConstants.KEY_ITEMS)) {
+ final JSONArray items = new JSONArray(localEventData.getString(IterableConstants.KEY_ITEMS));
+ final JSONArray processedItems = new JSONArray();
+ for (int j = 0; j < items.length(); j++) {
+ JSONObject processedItem = new JSONObject();
+ JSONObject item = items.getJSONObject(j);
+ Iterator keys = item.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ processedItem.put(IterableConstants.PURCHASE_ITEM_PREFIX + key, item.get(key));
+ }
+ processedItems.put(processedItem);
+ }
+ updatedItem.put(IterableConstants.PURCHASE_ITEM, processedItems);
+ }
+
+ if (localEventData.has(IterableConstants.KEY_DATA_FIELDS)) {
+ JSONObject dataFields = localEventData.getJSONObject(IterableConstants.KEY_DATA_FIELDS);
+ Iterator fieldKeys = dataFields.keys();
+ while (fieldKeys.hasNext()) {
+ String key = fieldKeys.next();
+ updatedItem.put(key, dataFields.get(key));
+ }
+ }
+
+ Iterator keys = localEventData.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ if (!key.equals(IterableConstants.KEY_ITEMS) && !key.equals(IterableConstants.KEY_DATA_FIELDS)) {
+ updatedItem.put(key, localEventData.get(key));
+ }
+ }
+ processedEvents.put(updatedItem);
+ } else if (localEventData.has(IterableConstants.SHARED_PREFS_EVENT_TYPE) && (
+ localEventData.get(IterableConstants.SHARED_PREFS_EVENT_TYPE).equals(IterableConstants.TRACK_UPDATE_CART))) {
+ JSONObject updatedItem = new JSONObject();
+ updatedItem.put(IterableConstants.KEY_EVENT_NAME, IterableConstants.UPDATE_CART);
+
+ if (localEventData.has(IterableConstants.KEY_ITEMS)) {
+ final JSONArray items = new JSONArray(localEventData.getString(IterableConstants.KEY_ITEMS));
+ final JSONArray processedItems = new JSONArray();
+ for (int j = 0; j < items.length(); j++) {
+ JSONObject processedItem = new JSONObject();
+ JSONObject item = items.getJSONObject(j);
+ Iterator keys = item.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ processedItem.put(IterableConstants.UPDATECART_ITEM_PREFIX + key, item.get(key));
+ }
+ processedItems.put(processedItem);
+ }
+ updatedItem.put(IterableConstants.KEY_ITEMS, processedItems);
+ }
+
+ if (localEventData.has(IterableConstants.KEY_DATA_FIELDS)) {
+ JSONObject dataFields = localEventData.getJSONObject(IterableConstants.KEY_DATA_FIELDS);
+ Iterator fieldKeys = dataFields.keys();
+ while (fieldKeys.hasNext()) {
+ String key = fieldKeys.next();
+ updatedItem.put(key, dataFields.get(key));
+ }
+ }
+
+ Iterator localEventDataKeys = localEventData.keys();
+ while (localEventDataKeys.hasNext()) {
+ String key = localEventDataKeys.next();
+ if (!key.equals(IterableConstants.KEY_ITEMS) && !key.equals(IterableConstants.KEY_DATA_FIELDS)) {
+ if (key.equals(IterableConstants.SHARED_PREFS_EVENT_TYPE)) {
+ updatedItem.put(key, IterableConstants.TRACK_EVENT);
+ } else {
+ updatedItem.put(key, localEventData.get(key));
+ }
+ }
+ }
+
+ processedEvents.put(updatedItem);
+ }
+ }
+ } catch (JSONException e) {
+ handleJSONException(e);
+ }
+ return processedEvents;
+ }
+
+ private JSONArray getNonCartEvents() {
+ JSONArray nonPurchaseEvents = new JSONArray();
+ try {
+ for (int i = 0; i < localStoredEventList.length(); i++) {
+ JSONObject localEventData = localStoredEventList.getJSONObject(i);
+ if (localEventData.has(IterableConstants.SHARED_PREFS_EVENT_TYPE)
+ && !localEventData.get(IterableConstants.SHARED_PREFS_EVENT_TYPE).equals(IterableConstants.TRACK_PURCHASE)
+ && !localEventData.get(IterableConstants.SHARED_PREFS_EVENT_TYPE).equals(IterableConstants.TRACK_UPDATE_CART)) {
+
+ JSONObject updatedItem = new JSONObject(localEventData.toString());
+ if (localEventData.has(IterableConstants.KEY_DATA_FIELDS)) {
+ JSONObject dataFields = localEventData.getJSONObject(IterableConstants.KEY_DATA_FIELDS);
+ Iterator fieldKeys = dataFields.keys();
+ while (fieldKeys.hasNext()) {
+ String key = fieldKeys.next();
+ updatedItem.put(key, dataFields.get(key));
+ }
+ }
+ nonPurchaseEvents.put(updatedItem);
+ }
+ }
+ } catch (JSONException e) {
+ handleJSONException(e);
+ }
+ return nonPurchaseEvents;
+ }
+
+ public boolean evaluateTree(JSONObject node, JSONArray localEventData) {
+ try {
+ if (node.has(IterableConstants.SEARCH_QUERIES)) {
+ String combinator = node.getString(IterableConstants.COMBINATOR);
+ JSONArray searchQueries = node.getJSONArray(IterableConstants.SEARCH_QUERIES);
+ if (combinator.equals("And")) {
+ for (int i = 0; i < searchQueries.length(); i++) {
+ if (!evaluateTree(searchQueries.getJSONObject(i), localEventData)) {
+ return false;
+ }
+ }
+ return true;
+ } else if (combinator.equals("Or")) {
+ for (int i = 0; i < searchQueries.length(); i++) {
+ if (evaluateTree(searchQueries.getJSONObject(i), localEventData)) {
+ return true;
+ }
+ }
+ return false;
+ } else if (combinator.equals("Not")) {
+ for (int i = 0; i < searchQueries.length(); i++) {
+ searchQueries.getJSONObject(i).put("isNot", true);
+ if (evaluateTree(searchQueries.getJSONObject(i), localEventData)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ } else if (node.has(IterableConstants.SEARCH_COMBO)) {
+ return evaluateSearchQueries(node, localEventData);
+ }
+ } catch (Exception e) {
+ handleException(e);
+ }
+ return false;
+ }
+
+ private boolean evaluateSearchQueries(JSONObject node, @NonNull JSONArray localEventData) throws JSONException {
+ for (int i = 0; i < localEventData.length(); i++) {
+ JSONObject eventData = localEventData.getJSONObject(i);
+ String trackingType = eventData.getString(IterableConstants.SHARED_PREFS_EVENT_TYPE);
+ String dataType = node.getString(IterableConstants.DATA_TYPE);
+ if (dataType.equals(trackingType)) {
+ JSONObject searchCombo = node.getJSONObject(IterableConstants.SEARCH_COMBO);
+ JSONArray searchQueries = searchCombo.getJSONArray(IterableConstants.SEARCH_QUERIES);
+ String combinator = searchCombo.getString(IterableConstants.COMBINATOR);
+ boolean isNot = node.has("isNot");
+
+ if (evaluateEvent(searchQueries, eventData, combinator)) {
+ if (node.has(IterableConstants.MIN_MATCH)) {
+ int minMatch = node.getInt(IterableConstants.MIN_MATCH) - 1;
+ node.put(IterableConstants.MIN_MATCH, minMatch);
+ if (minMatch > 0) {
+ continue;
+ }
+ }
+ if (isNot && !(i + 1 == localEventData.length())) {
+ continue;
+ }
+ return true;
+ } else if (isNot) {
+ return false;
+ }
+
+ }
+ }
+ return false;
+ }
+
+ private boolean evaluateEvent(JSONArray searchQueries, JSONObject eventData, String combinator) throws JSONException {
+ if (combinator.equals("And")) {
+ if (!evaluateFieldLogic(searchQueries, eventData)) {
+ return false;
+ }
+ return true;
+ } else if (combinator.equals("Or")) {
+ if (evaluateFieldLogic(searchQueries, eventData)) {
+ return true;
+ }
+ } else if (combinator.equals("Not")) {
+ return !evaluateFieldLogic(searchQueries, eventData);
+ }
+
+ return false;
+ }
+
+ private boolean evaluateFieldLogic(JSONArray searchQueries, JSONObject eventData) throws JSONException {
+ boolean itemMatchResult = false;
+ String itemKey = null;
+ if (eventData.has(IterableConstants.KEY_ITEMS)) {
+ itemKey = IterableConstants.KEY_ITEMS;
+ } else if (eventData.has(IterableConstants.PURCHASE_ITEM)) {
+ itemKey = IterableConstants.PURCHASE_ITEM;
+ }
+
+ if (itemKey != null) {
+ boolean result = false;
+ JSONArray items = new JSONArray(eventData.getString(itemKey));
+ for (int j = 0; j < items.length(); j++) {
+ JSONObject item = items.getJSONObject(j);
+ if (doesItemMatchQueries(searchQueries, item)) {
+ result = true;
+ break;
+ }
+ }
+ if (!result && doesItemCriteriaExists(searchQueries)) {
+ return false;
+ }
+ itemMatchResult = result;
+ }
+
+ ArrayList filteredDataKeys = new ArrayList<>();
+ Iterator localEventDataKeys = eventData.keys();
+ while (localEventDataKeys.hasNext()) {
+ String localEventDataKey = localEventDataKeys.next();
+ if (!localEventDataKey.equals(IterableConstants.KEY_ITEMS)) {
+ filteredDataKeys.add(localEventDataKey);
+ }
+ }
+
+ if (filteredDataKeys.size() == 0) {
+ return itemMatchResult;
+ }
+
+ JSONArray filteredSearchQueries = new JSONArray();
+ for (int i = 0; i < searchQueries.length(); i++) {
+ JSONObject searchQuery = searchQueries.getJSONObject(i);
+ String field = searchQuery.getString(IterableConstants.FIELD);
+ if (!field.startsWith(IterableConstants.PURCHASE_ITEM_PREFIX) && !field.startsWith(IterableConstants.UPDATECART_ITEM_PREFIX)) {
+ filteredSearchQueries.put(searchQuery);
+ }
+ }
+ if (filteredSearchQueries.length() == 0) {
+ return itemMatchResult;
+ }
+ boolean matchResult = false;
+ for (int k = 0; k < filteredSearchQueries.length(); k++) {
+ JSONObject searchQuery = filteredSearchQueries.getJSONObject(k);
+ String field = searchQuery.getString(IterableConstants.FIELD);
+ boolean isKeyExists = false;
+ if (searchQuery.getString(IterableConstants.DATA_TYPE).equals(IterableConstants.TRACK_EVENT) && searchQuery.getString("fieldType").equals("object") && searchQuery.getString(IterableConstants.COMPARATOR_TYPE).equals(MatchComparator.IS_SET)) {
+ final String eventName = eventData.getString(IterableConstants.KEY_EVENT_NAME);
+ if ((eventName.equals(IterableConstants.UPDATE_CART) && field.equals(eventName)) || field.equals(eventName)) {
+ matchResult = true;
+ continue;
+ }
+ } else {
+ for (String filteredDataKey : filteredDataKeys) {
+ if (field.equals(filteredDataKey)) {
+ isKeyExists = true;
+ }
+ }
+ }
+
+ // if field is a nested field
+ if (field.contains(".")) {
+ // separate the sub-fields into an array
+ String[] splitString = field.split("\\.");
+ // if event type is a custom event and event name equals the top-level sub-field
+ if ((eventData.has(IterableConstants.SHARED_PREFS_EVENT_TYPE) && eventData.get(IterableConstants.SHARED_PREFS_EVENT_TYPE).equals(IterableConstants.TRACK_EVENT))
+ && (eventData.has(IterableConstants.KEY_EVENT_NAME) && eventData.get(IterableConstants.KEY_EVENT_NAME).equals(splitString[0]))) {
+ // remove the event name from the separated sub-fields array
+ splitString = Arrays.copyOfRange(splitString, 1, splitString.length);
+ }
+
+ JSONObject fieldValue = eventData;
+ boolean isSubFieldArray = false;
+ boolean isSubMatch = false;
+
+ // loop through the separated fields array
+ for (String subField : splitString) {
+ // check if the current sub-field exists in the event data
+ if (fieldValue.has(subField)) {
+ // get the value of the current sub-field
+ Object subFieldValue = fieldValue.get(subField);
+ // check if the value is a JSONArray
+ if (subFieldValue instanceof JSONArray && ((JSONArray) subFieldValue).get(0) instanceof JSONObject) {
+ isSubFieldArray = true;
+ JSONArray subFieldValueArray = (JSONArray) subFieldValue;
+ // loop through the JSONArray
+ for (int i = 0; i < subFieldValueArray.length(); i++) {
+ // get the value of the current item in the JSONArray
+ Object item = subFieldValueArray.get(i);
+ JSONObject data = new JSONObject();
+
+ // loop through the separated fields array
+ // process array to allow individual items to be checked
+ for (int j = splitString.length - 1; j >= 0; j--) {
+ String split = splitString[j];
+ if (split.equals(subField)) {
+ data.put(split, item);
+ } else {
+ JSONObject temp = new JSONObject(data.toString());
+ data = new JSONObject();
+ data.put(split, temp);
+ }
+ }
+ // check if the current item matches the search queries
+ if (evaluateFieldLogic(searchQueries, mergeEventData(eventData, data))) {
+ // if item matches, set to true and break the loop
+ isSubMatch = true;
+ break;
+ }
+ }
+
+ } else if (subFieldValue instanceof JSONObject) {
+ // set field value to the JSONObject for next iteration
+ fieldValue = (JSONObject) subFieldValue;
+ }
+
+ // return result if sub-field is an array
+ if (isSubFieldArray) {
+ return isSubMatch;
+ }
+ }
+ }
+ Object valueFromObj = getFieldValue(eventData, field);
+ if (valueFromObj != null) {
+ matchResult = evaluateComparison(
+ searchQuery.getString(IterableConstants.COMPARATOR_TYPE),
+ valueFromObj,
+ searchQuery.has(IterableConstants.VALUES) ?
+ searchQuery.getJSONArray(IterableConstants.VALUES) :
+ searchQuery.getString(IterableConstants.VALUE)
+ );
+ if (matchResult) {
+ continue;
+ } else {
+ break;
+ }
+ }
+ } else if (isKeyExists) {
+ if (evaluateComparison(searchQuery.getString(IterableConstants.COMPARATOR_TYPE),
+ eventData.get(field),
+ searchQuery.has(IterableConstants.VALUES) ?
+ searchQuery.getJSONArray(IterableConstants.VALUES) :
+ searchQuery.getString(IterableConstants.VALUE))) {
+ matchResult = true;
+ continue;
+ }
+ }
+ matchResult = false;
+ break;
+ }
+ return matchResult;
+ }
+
+ private JSONObject mergeEventData(JSONObject eventData, JSONObject data) throws JSONException {
+ Iterator keys = data.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ eventData.put(key, data.get(key));
+ }
+ return eventData;
+ }
+
+ private Object getFieldValue(JSONObject data, String field) {
+ String[] fields = field.split("\\.");
+ try {
+ String eventType = data.getString(IterableConstants.SHARED_PREFS_EVENT_TYPE);
+
+ if (eventType.equals(IterableConstants.TRACK_EVENT)) {
+ String eventName = data.getString(IterableConstants.KEY_EVENT_NAME);
+ // if first field equals event name
+ if (fields[0].equals(eventName)) {
+ // remove the event name from the fields array
+ String[] newArray = new String[fields.length - 1];
+ System.arraycopy(fields, 1, newArray, 0, newArray.length);
+ fields = newArray;
+ }
+ }
+
+ JSONObject value = data;
+ Object fieldValue = null;
+
+ // loop through the fields array
+ for (String currentField : fields) {
+ // check if the current field exists event data
+ if (value.has(currentField)) {
+ // get the value of the current field
+ Object dataValue = value.get(currentField);
+ // check if the value is a JSONObject
+ if (dataValue instanceof JSONObject) {
+ // set the value to the JSONObject
+ value = value.getJSONObject(currentField);
+ } else {
+ // if the value is not a JSONObject, set the returned value to value of the current field
+ fieldValue = value.get(currentField);
+ }
+ } else {
+ break;
+ }
+ }
+ return fieldValue;
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ private boolean doesItemCriteriaExists(JSONArray searchQueries) throws JSONException {
+ for (int i = 0; i < searchQueries.length(); i++) {
+ String field = searchQueries.getJSONObject(i).getString(IterableConstants.FIELD);
+ if (field.startsWith(IterableConstants.UPDATECART_ITEM_PREFIX) || field.startsWith(IterableConstants.PURCHASE_ITEM_PREFIX)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ private boolean doesItemMatchQueries(JSONArray searchQueries, JSONObject item) throws JSONException {
+ JSONArray filterSearchQueries = new JSONArray();
+ for (int i = 0; i < searchQueries.length(); i++) {
+ JSONObject searchQuery = searchQueries.getJSONObject(i);
+ String field = searchQuery.getString(IterableConstants.FIELD);
+ if (field.startsWith(IterableConstants.UPDATECART_ITEM_PREFIX) || field.startsWith(IterableConstants.PURCHASE_ITEM_PREFIX)) {
+ if (!item.has(field)) {
+ return false;
+ }
+ filterSearchQueries.put(searchQuery);
+ }
+ }
+
+ if (filterSearchQueries.length() == 0) {
+ return false;
+ }
+
+ for (int j = 0; j < filterSearchQueries.length(); j++) {
+ JSONObject query = filterSearchQueries.getJSONObject(j);
+ String field = query.getString(IterableConstants.FIELD);
+ if (item.has(field)) {
+ if (!evaluateComparison(query.getString(IterableConstants.COMPARATOR_TYPE),
+ item.get(field),
+ query.has(IterableConstants.VALUES) ?
+ query.getJSONArray(IterableConstants.VALUES) :
+ query.getString(IterableConstants.VALUE))) {
+ return false;
+ }
+ }
+ }
+
+ if (filterSearchQueries.length() > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static String formattedDoubleValue(double d) {
+ if (d == (long) d)
+ return String.format("%d", (long) d);
+ else
+ return String.format("%s", d);
+ }
+
+ //
+ // comparison functions
+ //
+
+ private boolean evaluateComparison(String comparatorType, Object matchObj, Object valueToCompare) throws JSONException {
+ if (valueToCompare == null && !comparatorType.equals(MatchComparator.IS_SET)) {
+ return false;
+ }
+
+ valueToCompare = formatValueToCompare(valueToCompare);
+
+ switch (comparatorType) {
+ case MatchComparator.EQUALS:
+ return compareValueEquality(matchObj, valueToCompare);
+ case MatchComparator.DOES_NOT_EQUALS:
+ return !compareValueEquality(matchObj, valueToCompare);
+ case MatchComparator.IS_SET:
+ return issetCheck(matchObj);
+ case MatchComparator.GREATER_THAN:
+ case MatchComparator.LESS_THAN:
+ case MatchComparator.GREATER_THAN_OR_EQUAL_TO:
+ case MatchComparator.LESS_THAN_OR_EQUAL_TO:
+ return compareNumeric(matchObj, valueToCompare, comparatorType);
+ case MatchComparator.CONTAINS:
+ return compareContains(matchObj, String.valueOf(valueToCompare));
+ case MatchComparator.STARTS_WITH:
+ return compareStartsWith(matchObj, String.valueOf(valueToCompare));
+ case MatchComparator.MATCHES_REGEX:
+ return compareWithRegex(matchObj, String.valueOf(valueToCompare));
+ default:
+ return false;
+ }
+ }
+
+ private Object formatValueToCompare(Object valueToCompare) {
+ if (valueToCompare instanceof String && isDouble((String) valueToCompare)) {
+ return formattedDoubleValue(Double.parseDouble((String) valueToCompare));
+ }
+ return valueToCompare;
+ }
+
+ private boolean compareNumeric(Object matchObj, Object valueToCompare, String comparatorType) throws JSONException {
+ String comparisonOperator = getComparisonOperator(comparatorType);
+ return compareNumericValues(matchObj, String.valueOf(valueToCompare), comparisonOperator);
+ }
+
+ private String getComparisonOperator(String comparatorType) {
+ switch (comparatorType) {
+ case MatchComparator.GREATER_THAN:
+ return " > ";
+ case MatchComparator.LESS_THAN:
+ return " < ";
+ case MatchComparator.GREATER_THAN_OR_EQUAL_TO:
+ return " >= ";
+ case MatchComparator.LESS_THAN_OR_EQUAL_TO:
+ return " <= ";
+ default:
+ throw new IllegalArgumentException("Invalid comparator type: " + comparatorType);
+ }
+ }
+
+ private boolean issetCheck(Object matchObj) {
+ if (matchObj instanceof Object[]) {
+ return ((Object[]) matchObj).length > 0;
+ } else if (matchObj instanceof Map) {
+ return !((Map, ?>) matchObj).isEmpty();
+ } else {
+ return matchObj != null && !matchObj.equals("");
+ }
+ }
+
+ private boolean compareValueEquality(Object sourceTo, Object stringValue) throws JSONException {
+ if (sourceTo instanceof JSONArray) {
+ return compareWithJSONArray((JSONArray) sourceTo, stringValue);
+ } else if (stringValue instanceof JSONArray) {
+ return compareWithJSONArray((JSONArray) stringValue, sourceTo);
+ } else if (sourceTo instanceof String || stringValue instanceof String) {
+ return compareWithParsedValues(sourceTo, stringValue);
+ } else {
+ return sourceTo.equals(stringValue);
+ }
+ }
+
+ private boolean compareWithJSONArray(JSONArray jsonArray, Object valueToCompare) throws JSONException {
+ for (int i = 0; i < jsonArray.length(); i++) {
+ if (compareValueEquality(jsonArray.get(i), valueToCompare)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean compareWithParsedValues(Object sourceTo, Object stringValue) {
+ String stringVal = stringValue.toString();
+
+ if (sourceTo instanceof Double && isDouble(stringVal)) {
+ return sourceTo.equals(Double.parseDouble(stringVal));
+ } else if (sourceTo instanceof Integer && isInteger(stringVal)) {
+ return sourceTo.equals(Integer.parseInt(stringVal));
+ } else if (sourceTo instanceof Long && isLong(stringVal)) {
+ return sourceTo.equals(Long.parseLong(stringVal));
+ } else if (sourceTo instanceof Boolean && isBoolean(stringVal)) {
+ return sourceTo.equals(Boolean.parseBoolean(stringVal));
+ } else {
+ return sourceTo.equals(stringValue);
+ }
+ }
+
+ private boolean compareArrayNumericValue(Object sourceTo, String stringValue, String compareOperator) throws JSONException {
+ JSONArray jsonArraySourceTo = (JSONArray) sourceTo;
+ boolean isMatched = false;
+ for (int i = 0; i < jsonArraySourceTo.length(); i++) {
+ if (compareNumericValues(jsonArraySourceTo.get(i), stringValue, compareOperator)) {
+ isMatched = true;
+ }
+ }
+ return isMatched;
+ }
+
+ private boolean compareNumericValues(Object sourceTo, String stringValue, String compareOperator) throws JSONException {
+ if (sourceTo instanceof JSONArray) {
+ return compareArrayNumericValue(sourceTo, stringValue, compareOperator);
+ } else if (isDouble(stringValue)) {
+ double sourceNumber = getDoubleValue(sourceTo);
+ double numericValue = Double.parseDouble(stringValue);
+ switch (compareOperator.trim()) {
+ case ">":
+ return sourceNumber > numericValue;
+ case "<":
+ return sourceNumber < numericValue;
+ case ">=":
+ return sourceNumber >= numericValue;
+ case "<=":
+ return sourceNumber <= numericValue;
+ default:
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private boolean compareContains(Object sourceTo, String stringValue) throws JSONException {
+ if (sourceTo instanceof JSONArray) {
+ JSONArray jsonArraySourceTo = (JSONArray) sourceTo;
+
+ if (jsonArraySourceTo.get(0) instanceof String) {
+ // check if any string in the array contains the string
+ return arrayContains(jsonArraySourceTo, stringValue);
+ }
+ } else if (sourceTo instanceof String) {
+ return ((String) sourceTo).contains(stringValue);
+ }
+ return false;
+ }
+
+ private boolean arrayContains(JSONArray jsonArray, String stringValue) throws JSONException {
+ for (int i = 0; i < jsonArray.length(); i++) {
+ if (jsonArray.getString(i).contains(stringValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean compareStartsWith(Object sourceTo, String stringValue) throws JSONException {
+ if (sourceTo instanceof JSONArray) {
+ JSONArray jsonArraySourceTo = (JSONArray) sourceTo;
+
+ if (jsonArraySourceTo.get(0) instanceof String) {
+ // check if any string in the array starts with string
+ return anyStartsWith(jsonArraySourceTo, stringValue);
+ }
+ } else if (sourceTo instanceof String) {
+ return ((String) sourceTo).startsWith(stringValue);
+ }
+
+ return false;
+ }
+
+ private boolean anyStartsWith(JSONArray jsonArray, String stringValue) throws JSONException {
+ for (int i = 0; i < jsonArray.length(); i++) {
+ if (jsonArray.getString(i).startsWith(stringValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean compareWithRegex(Object sourceTo, String pattern) throws JSONException {
+ try {
+ Pattern regexPattern = Pattern.compile(pattern);
+
+ // If the source is a JSONArray
+ if (sourceTo instanceof JSONArray) {
+ JSONArray jsonArraySourceTo = (JSONArray) sourceTo;
+
+ if (jsonArraySourceTo.get(0) instanceof String) {
+ // check if any string in the array matches the regex
+ return anyMatchesRegex(jsonArraySourceTo, regexPattern);
+ }
+ } else if (sourceTo instanceof String) {
+ return regexPattern.matcher((String) sourceTo).matches();
+ }
+
+ } catch (PatternSyntaxException e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ private boolean anyMatchesRegex(JSONArray jsonArray, Pattern regexPattern) throws JSONException {
+ for (int i = 0; i < jsonArray.length(); i++) {
+ if (regexPattern.matcher(jsonArray.getString(i)).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private double getDoubleValue(Object value) {
+ if (value instanceof Double) {
+ return (Double) value;
+ } else if (value instanceof Integer) {
+ return (double) (Integer) value;
+ } else if (value instanceof Long) {
+ return (double) (Long) value;
+ } else if (value instanceof String && isDouble((String) value)) {
+ return Double.parseDouble((String) value);
+ }
+ return 0.0;
+ }
+
+ private boolean isDouble(String value) {
+ try {
+ Double.parseDouble(value);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ private boolean isInteger(String value) {
+ try {
+ Integer.parseInt(value);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ private boolean isLong(String value) {
+ try {
+ Long.parseLong(value);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ private boolean isBoolean(String value) {
+ return "true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value);
+ }
+
+ private void handleException(Exception e) {
+ IterableLogger.e("Exception occurred", e.toString());
+ e.printStackTrace();
+ }
+
+ private void handleJSONException(JSONException e) {
+ IterableLogger.e("JSONException occurred", e.toString());
+ e.printStackTrace();
+ }
+}
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/util/IterableJwtGenerator.java b/iterableapi/src/main/java/com/iterable/iterableapi/util/IterableJwtGenerator.java
new file mode 100644
index 000000000..9eeb8128b
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/util/IterableJwtGenerator.java
@@ -0,0 +1,88 @@
+package com.iterable.iterableapi.util;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Base64;
+
+@RequiresApi(api = Build.VERSION_CODES.O)
+public class IterableJwtGenerator {
+
+ static Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
+ private static final String algorithm = "HmacSHA256";
+ // Iterable enforces a 1-year maximum token lifetime
+ private static final Duration maxTokenLifetime = Duration.ofDays(365);
+ private static long millisToSeconds(long millis) {
+ return millis / 1000;
+ }
+ private static final String encodedHeader = encoder.encodeToString(
+ "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8)
+ );
+ /**
+ * Generates a JWT from the provided secret, header, and payload. Does not
+ * validate the header or payload.
+ *
+ * @param secret Your organization's shared secret with Iterable
+ * @param payload The JSON payload
+ *
+ * @return a signed JWT
+ */
+ public static String generateToken(String secret, String payload) {
+ try {
+ String encodedPayload = encoder.encodeToString(
+ payload.getBytes(StandardCharsets.UTF_8)
+ );
+ String encodedHeaderAndPayload = encodedHeader + "." + encodedPayload;
+ // HMAC setup
+ Mac hmac = Mac.getInstance(algorithm);
+ SecretKeySpec keySpec = new SecretKeySpec(
+ secret.getBytes(StandardCharsets.UTF_8), algorithm
+ );
+ hmac.init(keySpec);
+ String signature = encoder.encodeToString(
+ hmac.doFinal(
+ encodedHeaderAndPayload.getBytes(StandardCharsets.UTF_8)
+ )
+ );
+ return encodedHeaderAndPayload + "." + signature;
+ } catch (Exception e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+ /**
+ * Generates a JWT (issued now, expires after the provided duration).
+ *
+ * @param secret Your organization's shared secret with Iterable.
+ * @param duration The token's expiration time. Up to one year.
+ * @param email The email to included in the token, or null.
+ * @param userId The userId to include in the token, or null.
+ *
+ * @return A JWT string
+ */
+ public static String generateToken(
+ String secret, Duration duration, String email, String userId) {
+ if (duration.compareTo(maxTokenLifetime) > 0) {
+ throw new IllegalArgumentException("Duration must be one year or less.");
+ }
+ if ((userId != null && email != null) || (userId == null && email == null)) {
+ throw new IllegalArgumentException("The token must include a userId or email, but not both.");
+ }
+ long now = millisToSeconds(System.currentTimeMillis());
+ String payload;
+ if (userId != null) {
+ payload = String.format(
+ "{ \"userId\": \"%s\", \"iat\": %d, \"exp\": %d }",
+ userId, now, now + millisToSeconds(duration.toMillis()));
+ } else {
+ payload = String.format(
+ "{ \"email\": \"%s\", \"iat\": %d, \"exp\": %d }",
+ email, now, now + millisToSeconds(duration.toMillis()));
+ }
+ return generateToken(secret, payload);
+ }
+}
\ No newline at end of file
diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/util/MatchComparator.java b/iterableapi/src/main/java/com/iterable/iterableapi/util/MatchComparator.java
new file mode 100644
index 000000000..b05af7a4f
--- /dev/null
+++ b/iterableapi/src/main/java/com/iterable/iterableapi/util/MatchComparator.java
@@ -0,0 +1,14 @@
+package com.iterable.iterableapi.util;
+
+public class MatchComparator {
+ public static final String EQUALS = "Equals";
+ public static final String DOES_NOT_EQUALS = "DoesNotEqual";
+ public static final String IS_SET = "IsSet";
+ public static final String GREATER_THAN = "GreaterThan";
+ public static final String LESS_THAN = "LessThan";
+ public static final String GREATER_THAN_OR_EQUAL_TO = "GreaterThanOrEqualTo";
+ public static final String LESS_THAN_OR_EQUAL_TO = "LessThanOrEqualTo";
+ public static final String CONTAINS = "Contains";
+ public static final String STARTS_WITH = "StartsWith";
+ public static final String MATCHES_REGEX = "MatchesRegex";
+}
\ No newline at end of file
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthJWTTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthJWTTests.java
new file mode 100644
index 000000000..c5520bf5b
--- /dev/null
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthJWTTests.java
@@ -0,0 +1,267 @@
+package com.iterable.iterableapi;
+
+import static android.os.Looper.getMainLooper;
+import static com.iterable.iterableapi.IterableConstants.HEADER_SDK_AUTH_FORMAT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.iterable.iterableapi.unit.PathBasedQueueDispatcher;
+
+import org.json.JSONException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
+public class IterableApiAuthJWTTests extends BaseTest {
+
+ private MockWebServer server;
+ private IterableAuthHandler authHandler;
+ private PathBasedQueueDispatcher dispatcher;
+
+ private final String validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAiZW1haWwiOiAidGVzdEBleGFtcGxlLmNvbSIsICJpYXQiOiAxNzI5MjUyNDE3LCAiZXhwIjogMTcyOTg1NzIxNyB9.m-O6ksCv9OR-cF0RdiHB8VW_NwWJHVXChipbcFmIChg";
+ private final String newJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE5MTYyMzkwMjJ9.dMD3MLuHTiO-Qy9PvOoMchNM4CzFIgI7jKVrRtlqlM0";
+
+ private final String criteriaMockData = "{\n" +
+ " \"count\":2,\n" +
+ " \"criteriaSets\":[\n" +
+ " {\n" +
+ " \"criteriaId\":43,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.price\",\n" +
+ " \"fieldType\":\"double\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":2,\n" +
+ " \"value\":\"4.67\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.quantity\",\n" +
+ " \"fieldType\":\"long\",\n" +
+ " \"comparatorType\":\"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":3,\n" +
+ " \"valueLong\":2,\n" +
+ " \"value\":\"2\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"criteriaId\":5678,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"itblInternal.emailDomain\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"id\":6,\n" +
+ " \"value\":\"gmail.com\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"eventName\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":9,\n" +
+ " \"value\":\"processing_cancelled\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"createdAt\",\n" +
+ " \"fieldType\":\"date\",\n" +
+ " \"comparatorType\":\"GreaterThan\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":10,\n" +
+ " \"dateRange\":{\n" +
+ " \n" +
+ " },\n" +
+ " \"isRelativeDate\":false,\n" +
+ " \"value\":\"1688194800000\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ @Before
+ public void setUp() throws IOException {
+ server = new MockWebServer();
+ dispatcher = new PathBasedQueueDispatcher();
+ server.setDispatcher(dispatcher);
+ IterableApi.overrideURLEndpointPath(server.url("").toString());
+ reInitIterableApi();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ server.shutdown();
+ server = null;
+ }
+
+ private void reInitIterableApi() {
+ IterableApi.sharedInstance = new IterableApi();
+ authHandler = mock(IterableAuthHandler.class);
+ IterableTestUtils.createIterableApiNew(builder -> builder.setAuthHandler(authHandler), null);
+ IterableConfig iterableConfig = new IterableConfig.Builder().setEnableUnknownUserActivation(true).setAuthHandler(authHandler).build();
+ IterableApi.initialize(getContext(), "fake_key", iterableConfig);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+ setCriteria(criteriaMockData);
+ }
+
+ private void setCriteria(String criteria) {
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_CRITERIA, criteria);
+ editor.apply();
+ }
+
+ private void addResponse(String endPoint) {
+ dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}"));
+ }
+
+ private String getEventData() {
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ return sharedPref.getString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ }
+
+ private void triggerTrackPurchaseEvent(String id, String name, double price, int quantity) throws JSONException {
+ List items = new ArrayList<>();
+ items.add(new CommerceItem(id, name, price, quantity));
+ IterableApi.getInstance().trackPurchase(4, items);
+ }
+
+ @Test
+ public void testCriteriaUserIdTokenCheckPass() throws Exception {
+ String userId = "testUserId";
+ IterableApi.getInstance().setUserId(userId);
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ assertNull(IterableApi.getInstance().getAuthToken());
+
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) {
+ }
+ final String userId1 = "testUser1";
+
+ doReturn(validJWT).when(authHandler).onAuthTokenRequested();
+ IterableApi.getInstance().setUserId(userId1);
+ RecordedRequest recordedRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(recordedRequest);
+ shadowOf(getMainLooper()).idle();
+ assertEquals(userId1, IterableApi.getInstance().getUserId());
+ assertEquals(HEADER_SDK_AUTH_FORMAT + validJWT, recordedRequest.getHeader("Authorization"));
+
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) {
+ }
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+ assertEquals("", getEventData());
+
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) {
+ }
+ final String userId2 = "testUser2";
+
+ doReturn(newJWT).when(authHandler).onAuthTokenRequested();
+ IterableApi.getInstance().setUserId(userId2);
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ shadowOf(getMainLooper()).idle();
+ assertEquals(userId2, IterableApi.getInstance().getUserId());
+ assertEquals(HEADER_SDK_AUTH_FORMAT + newJWT, mergeRequest.getHeader("Authorization"));
+ }
+
+ @Test
+ public void testCriteriaEmailIdTokenCheckPass() throws Exception {
+ String emailId = "testUserId@example.com";
+ IterableApi.getInstance().setEmail(emailId);
+ assertEquals(emailId, IterableApi.getInstance().getEmail());
+ assertNull(IterableApi.getInstance().getAuthToken());
+
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) {
+ }
+ final String emailId1 = "testUser1@example.com";
+
+ doReturn(validJWT).when(authHandler).onAuthTokenRequested();
+ IterableApi.getInstance().setEmail(emailId1);
+ RecordedRequest recordedRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(recordedRequest);
+ shadowOf(getMainLooper()).idle();
+ assertEquals(emailId1, IterableApi.getInstance().getEmail());
+ assertEquals(HEADER_SDK_AUTH_FORMAT + validJWT, recordedRequest.getHeader("Authorization"));
+
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) {
+ }
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+ assertEquals("", getEventData());
+
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) {
+ }
+ final String emailId2 = "testUser2@example.com";
+
+ doReturn(newJWT).when(authHandler).onAuthTokenRequested();
+ IterableApi.getInstance().setEmail(emailId2);
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ shadowOf(getMainLooper()).idle();
+ assertEquals(emailId2, IterableApi.getInstance().getEmail());
+ assertEquals(HEADER_SDK_AUTH_FORMAT + newJWT, mergeRequest.getHeader("Authorization"));
+ }
+}
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java
index 56efd4547..12d1b473b 100644
--- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiAuthTests.java
@@ -492,7 +492,7 @@ public void testAuthTokenRefreshPausesOnBackground() throws Exception {
shadowOf(getMainLooper()).runToEndOfTasks();
// Request auth token which should set a timer for expiration refresh
- authManager.requestNewAuthToken(false);
+ authManager.requestNewAuthToken(false, null);
shadowOf(getMainLooper()).runToEndOfTasks();
// The timer might be null if the token is considered expired, so let's test the behavior
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java
new file mode 100644
index 000000000..d09f948ae
--- /dev/null
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCriteriaFetchTests.java
@@ -0,0 +1,194 @@
+package com.iterable.iterableapi;
+
+import static android.os.Looper.getMainLooper;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Activity;
+
+import com.iterable.iterableapi.unit.PathBasedQueueDispatcher;
+
+import junit.framework.Assert;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.robolectric.Robolectric;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
+public class IterableApiCriteriaFetchTests extends BaseTest {
+ private MockWebServer server;
+ private PathBasedQueueDispatcher dispatcher;
+
+ @Before
+ public void setUp() {
+ server = new MockWebServer();
+ dispatcher = new PathBasedQueueDispatcher();
+ server.setDispatcher(dispatcher);
+
+ reInitIterableApi();
+ IterableApi.overrideURLEndpointPath(server.url("").toString());
+
+ IterableConfig iterableConfig = new IterableConfig.Builder().setEnableUnknownUserActivation(true).build();
+ IterableApi.initialize(getContext(), "apiKey", iterableConfig);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+ }
+
+ private void reInitIterableApi() {
+ IterableApi.sharedInstance = new IterableApi();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ server.shutdown();
+ server = null;
+ IterableApi.getInstance().setUserId(null);
+ IterableApi.getInstance().setEmail(null);
+
+ // Add these cleanup steps
+ IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext());
+ IterableActivityMonitor.instance = new IterableActivityMonitor();
+
+ // Clear any pending handlers
+ shadowOf(getMainLooper()).idle();
+ }
+
+ private void addResponse(String endPoint) {
+ dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}"));
+ }
+
+ @Test
+ public void testForegroundCriteriaFetchWhenConditionsMet() throws Exception {
+ // Clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // Mock responses for expected endpoints
+ addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST);
+
+ // Initialize with unknown user activation and foreground fetch enabled
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setEnableForegroundCriteriaFetch(true)
+ .build();
+
+ IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext());
+ IterableActivityMonitor.instance = new IterableActivityMonitor();
+
+ // Initialize API
+ IterableApi.initialize(getContext(), "apiKey", config);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+
+ // Verify first criteria fetch when consent is given
+ RecordedRequest firstCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ Assert.assertNotNull("First criteria request should be made", firstCriteriaRequest);
+ assertTrue("First request URL should contain getCriteria endpoint",
+ firstCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST));
+
+ // Simulate app coming to foreground
+ Robolectric.buildActivity(Activity.class).create().start().resume();
+ shadowOf(getMainLooper()).idle();
+
+ // Verify criteria fetch request was made
+ RecordedRequest secondCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ Assert.assertNotNull("Criteria request should be made on foreground", secondCriteriaRequest);
+ assertTrue("Request URL should contain getCriteria endpoint",
+ secondCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST));
+
+ // Clean up
+ IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext());
+ IterableActivityMonitor.instance = new IterableActivityMonitor();
+ }
+
+ @Test
+ public void testCriteriaFetchNotCalledWhenDisabled() throws Exception {
+ // Initialize with foreground fetch disabled
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setEnableForegroundCriteriaFetch(false)
+ .build();
+
+ // Initialize API and set visitor tracking
+ IterableApi.initialize(getContext(), "apiKey", config);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+
+ // Clear out any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // Simulate app coming to foreground
+ Robolectric.buildActivity(Activity.class).create().start().resume();
+ shadowOf(getMainLooper()).idle();
+
+ // Should only get remote config request after foreground
+ RecordedRequest configRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ Assert.assertNotNull("Should have remote config request", configRequest);
+ assertTrue("Should be a remote configuration request",
+ configRequest.getPath().contains(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION));
+
+ // No more requests
+ RecordedRequest extraRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ Assert.assertNull("Should not have any additional requests", extraRequest);
+
+ // Clean up
+ IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext());
+ IterableActivityMonitor.instance = new IterableActivityMonitor();
+ }
+
+ @Test
+ public void testForegroundCriteriaFetchWithCooldown() throws Exception {
+ // Clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // Mock responses
+ addResponse(IterableConstants.ENDPOINT_CRITERIA_LIST);
+ addResponse(IterableConstants.ENDPOINT_GET_REMOTE_CONFIGURATION);
+
+ // Initialize with required config
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setEnableForegroundCriteriaFetch(true)
+ .build();
+
+ IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext());
+ IterableActivityMonitor.instance = new IterableActivityMonitor();
+
+ // Initialize API
+ IterableApi.initialize(getContext(), "apiKey", config);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+
+ // Verify first criteria fetch when consent is given
+ RecordedRequest firstCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ Assert.assertNotNull("First criteria request should be made", firstCriteriaRequest);
+ assertTrue("First request URL should contain getCriteria endpoint",
+ firstCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST));
+
+ // First foreground
+ Robolectric.buildActivity(Activity.class).create().start().resume();
+ shadowOf(getMainLooper()).idle();
+
+ // Verify second criteria fetch
+ RecordedRequest secondCriteriaRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ Assert.assertNotNull("Second criteria request should be made", firstCriteriaRequest);
+ assertTrue("Second request URL should contain getCriteria endpoint",
+ secondCriteriaRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST));
+
+ // Immediate second foreground
+ Robolectric.buildActivity(Activity.class).create().start().resume();
+ shadowOf(getMainLooper()).idle();
+
+ // Verify no criteria requests during cooldown period
+ RecordedRequest cooldownRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertFalse("Second request URL should contain getCriteria endpoint",
+ cooldownRequest.getPath().contains(IterableConstants.ENDPOINT_CRITERIA_LIST));
+
+ // Clean up
+ IterableActivityMonitor.getInstance().unregisterLifecycleCallbacks(getContext());
+ IterableActivityMonitor.instance = new IterableActivityMonitor();
+ }
+}
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCustomEventTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCustomEventTests.java
new file mode 100644
index 000000000..e1c4c6639
--- /dev/null
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiCustomEventTests.java
@@ -0,0 +1,176 @@
+package com.iterable.iterableapi;
+
+import static android.os.Looper.getMainLooper;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.iterable.iterableapi.unit.PathBasedQueueDispatcher;
+
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
+public class IterableApiCustomEventTests extends BaseTest {
+ private MockWebServer server;
+ private PathBasedQueueDispatcher dispatcher;
+
+ @Before
+ public void setUp() {
+ server = new MockWebServer();
+ dispatcher = new PathBasedQueueDispatcher();
+ server.setDispatcher(dispatcher);
+ reInitIterableApi();
+ IterableApi.overrideURLEndpointPath(server.url("").toString());
+ IterableConfig iterableConfig = new IterableConfig.Builder().setEnableUnknownUserActivation(true).build();
+ IterableApi.initialize(getContext(), "apiKey", iterableConfig);
+
+ String criteriaMockData = "{\n" +
+ " \"count\": 1,\n" +
+ " \"criteriaSets\": [\n" +
+ " {\n" +
+ " \"criteriaId\": \"423\",\n" +
+ " \"name\": \"animal-found Test Cases\",\n" +
+ " \"createdAt\": 1726648931809,\n" +
+ " \"updatedAt\": 1726648931809,\n" +
+ " \"searchQuery\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"searchCombo\": {\n" +
+ " \"combinator\": \"And\",\n" +
+ " \"searchQueries\": [\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal-found.count\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"6\",\n" +
+ " \"fieldType\": \"long\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal-found.type\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"cat\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"animal-found.vaccinated\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"true\",\n" +
+ " \"fieldType\": \"boolean\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\": \"customEvent\",\n" +
+ " \"field\": \"eventName\",\n" +
+ " \"comparatorType\": \"Equals\",\n" +
+ " \"value\": \"animal-found\",\n" +
+ " \"fieldType\": \"string\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+
+ setCriteria(criteriaMockData);
+ }
+
+
+ private void reInitIterableApi() {
+ IterableApi.sharedInstance = new IterableApi();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ server.shutdown();
+ server = null;
+ clearEventData();
+ IterableApi.getInstance().setUserId(null);
+ IterableApi.getInstance().setEmail(null);
+ }
+
+ private void clearEventData() {
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ editor.apply();
+ }
+ private void setCriteria(String criteria) {
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_CRITERIA, criteria);
+ editor.apply();
+ }
+ private void addResponse(String endPoint) {
+ dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}"));
+ }
+
+ @Test
+ public void testCustomEventTrackApi() throws Exception {
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK);
+ final String userId = "testUser2";
+
+ IterableIdentityResolution identityRes = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setUserId(userId, identityRes);
+
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+ JSONObject customEventItem = new JSONObject("{\n" +
+ " \"dataFields\": " +
+ " {\n" +
+ " \"type\":\"cat\",\n" +
+ " \"count\":6,\n" +
+ " \"vaccinated\":true\n" +
+ " }\n" +
+ "}");
+
+ JSONObject items = new JSONObject(String.valueOf(customEventItem.getJSONObject(IterableConstants.KEY_DATA_FIELDS)));
+ IterableApi.getInstance().track("animal-found", items);
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ shadowOf(getMainLooper()).idle();
+ JSONObject requestJson = new JSONObject(mergeRequest.getBody().readUtf8());
+
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK, mergeRequest.getPath());
+ assertTrue("dataField should be set in the request", requestJson.has(IterableConstants.KEY_DATA_FIELDS));
+
+ JSONObject dataFieldObject = requestJson.getJSONObject(IterableConstants.KEY_DATA_FIELDS);
+
+ assertTrue(dataFieldObject.has("type"));
+ assertTrue(dataFieldObject.has("count"));
+ assertTrue(dataFieldObject.has("vaccinated"));
+
+ assertFalse(dataFieldObject.has("animal-found.type"));
+ assertFalse(dataFieldObject.has("animal-found.count"));
+ assertFalse(dataFieldObject.has("animal-found.vaccinated"));
+
+ assertEquals(dataFieldObject.getString("type"), "cat");
+ assertEquals(dataFieldObject.getInt("count"), 6);
+ assertTrue(dataFieldObject.getBoolean("vaccinated"));
+ }
+}
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java
new file mode 100644
index 000000000..8c37c6bdf
--- /dev/null
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiMergeUserEmailTests.java
@@ -0,0 +1,1003 @@
+package com.iterable.iterableapi;
+
+import static android.os.Looper.getMainLooper;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.iterable.iterableapi.unit.PathBasedQueueDispatcher;
+
+import org.json.JSONException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
+public class IterableApiMergeUserEmailTests extends BaseTest {
+ private MockWebServer server;
+ private PathBasedQueueDispatcher dispatcher;
+ private final String criteriaMockData = "{\n" +
+ " \"count\":2,\n" +
+ " \"criteriaSets\":[\n" +
+ " {\n" +
+ " \"criteriaId\":43,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.price\",\n" +
+ " \"fieldType\":\"double\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":2,\n" +
+ " \"value\":\"4.67\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"shoppingCartItems.quantity\",\n" +
+ " \"fieldType\":\"long\",\n" +
+ " \"comparatorType\":\"GreaterThanOrEqualTo\",\n" +
+ " \"dataType\":\"purchase\",\n" +
+ " \"id\":3,\n" +
+ " \"valueLong\":2,\n" +
+ " \"value\":\"2\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"criteriaId\":5678,\n" +
+ " \"searchQuery\":{\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"combinator\":\"Or\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"itblInternal.emailDomain\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"user\",\n" +
+ " \"id\":6,\n" +
+ " \"value\":\"gmail.com\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"searchCombo\":{\n" +
+ " \"combinator\":\"And\",\n" +
+ " \"searchQueries\":[\n" +
+ " {\n" +
+ " \"field\":\"eventName\",\n" +
+ " \"fieldType\":\"string\",\n" +
+ " \"comparatorType\":\"Equals\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":9,\n" +
+ " \"value\":\"processing_cancelled\"\n" +
+ " },\n" +
+ " {\n" +
+ " \"field\":\"createdAt\",\n" +
+ " \"fieldType\":\"date\",\n" +
+ " \"comparatorType\":\"GreaterThan\",\n" +
+ " \"dataType\":\"customEvent\",\n" +
+ " \"id\":10,\n" +
+ " \"dateRange\":{\n" +
+ " \n" +
+ " },\n" +
+ " \"isRelativeDate\":false,\n" +
+ " \"value\":\"1688194800000\"\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " ]\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ "}";
+ @Before
+ public void setUp() {
+ server = new MockWebServer();
+ dispatcher = new PathBasedQueueDispatcher();
+ server.setDispatcher(dispatcher);
+ reInitIterableApi();
+ IterableApi.overrideURLEndpointPath(server.url("").toString());
+ IterableConfig iterableConfig = new IterableConfig.Builder().setEnableUnknownUserActivation(true).build();
+ IterableApi.initialize(getContext(), "apiKey", iterableConfig);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+ setCriteria(criteriaMockData);
+ }
+
+ private void reInitIterableApi() {
+ IterableApi.sharedInstance = new IterableApi();
+ }
+
+ @After
+ public void tearDown() throws IOException {
+ server.shutdown();
+ server = null;
+ clearEventData();
+ IterableApi.getInstance().setUserId(null);
+ IterableApi.getInstance().setEmail(null);
+ }
+
+ private String getEventData() {
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ return sharedPref.getString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ }
+ private void clearEventData() {
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, "");
+ editor.apply();
+ }
+ private void setCriteria(String criteria) {
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putString(IterableConstants.SHARED_PREFS_CRITERIA, criteria);
+ editor.apply();
+ }
+ private void triggerTrackPurchaseEvent(String id, String name, double price, int quantity) throws JSONException {
+ List items = new ArrayList<>();
+ items.add(new CommerceItem(id, name, price, quantity));
+ IterableApi.getInstance().trackPurchase(4, items);
+ }
+ private void addResponse(String endPoint) {
+ dispatcher.enqueueResponse("/" + endPoint, new MockResponse().setResponseCode(200).setBody("{}"));
+ }
+
+ // all userId tests
+ @Test
+ public void testCriteriaNotMetUserIdDefault() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set user id
+ final String userId = "testUser2";
+ IterableApi.getInstance().setUserId(userId);
+
+ // check that request was sent to purchase endpoint on event replay
+ RecordedRequest purchaseRequest2 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(purchaseRequest2);
+ assertEquals(("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE), purchaseRequest2.getPath());
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(request);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), request.getPath());
+
+ // check that user id was set
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testCriteriaNotMetUserIdReplayTrueMergeFalse() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set user id
+ final String userId = "testUser2";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setUserId(userId, identityResolution);
+
+ // check that request was sent to purchase endpoint on event replay
+ RecordedRequest purchaseRequest2 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(purchaseRequest2);
+ assertEquals(("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE), purchaseRequest2.getPath());
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest consentRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(consentRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), consentRequest.getPath());
+
+ // check that user id was set
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testCriteriaNotMetUserIdReplayFalseMergeFalse() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set user id
+ final String userId = "testUser2";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(false, false);
+ IterableApi.getInstance().setUserId(userId, identityResolution);
+
+ // check that request was not sent to merge endpoint or track consent endpoint
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(request);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), request.getPath());
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_TRACK_CONSENT), request.getPath());
+
+ // check that user id was set
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testCriteriaNotMetUserIdReplayFalseMergeTrue() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set user id
+ final String userId = "testUser2";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(false, true);
+ IterableApi.getInstance().setUserId(userId, identityResolution);
+
+ // check that request was not sent to merge endpoint or track consent endpoint
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(request);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), request.getPath());
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_TRACK_CONSENT), request.getPath());
+
+ // check that user id was set
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testCriteriaMetUserIdDefault() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+
+ // check if request was sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());
+
+ // check if request was sent to track purchase endpoint
+ RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", purchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath());
+
+ // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
+ RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("InApp messages request should be sent", inAppRequest);
+ assertTrue("InApp messages request path should start with correct endpoint",
+ inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set user id
+ final String userId = "testUser2";
+ IterableApi.getInstance().setUserId(userId);
+
+ // check that request was sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());
+
+ // check that user id was set
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testCriteriaMetUserIdMergeFalse() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+
+ // check if request was sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());
+
+ // check if request was sent to track purchase endpoint
+ RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", purchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath());
+
+ // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
+ RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("InApp messages request should be sent", inAppRequest);
+ assertTrue("InApp messages request path should start with correct endpoint",
+ inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set user id
+ final String userId = "testUser2";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setUserId(userId, identityResolution);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());
+
+ // check that user id was set
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testCriteriaMetUserIdMergeTrue() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+
+ // check if request was sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());
+
+ // check if request was sent to track purchase endpoint
+ RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", purchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath());
+
+ // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
+ RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("InApp messages request should be sent", inAppRequest);
+ assertTrue("InApp messages request path should start with correct endpoint",
+ inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set user id
+ final String userId = "testUser2";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableApi.getInstance().setUserId(userId, identityResolution);
+
+ // check that request was sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());
+
+ // check that user id was set
+ assertEquals(userId, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testIdentifiedUserIdDefault() throws Exception {
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set user id
+ final String userId1 = "testUser1";
+ IterableApi.getInstance().setUserId(userId1);
+
+ // check that user id was set
+ assertEquals(userId1, IterableApi.getInstance().getUserId());
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set different user id
+ final String userId2 = "testUser2";
+ IterableApi.getInstance().setUserId(userId2);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), mergeRequest.getPath());
+
+ // check that user id was set
+ assertEquals(userId2, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testIdentifiedUserIdMergeFalse() throws Exception {
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set user id
+ final String userId1 = "testUser1";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setUserId(userId1, identityResolution);
+
+ // check that user id was set
+ assertEquals(userId1, IterableApi.getInstance().getUserId());
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set different user id
+ final String userId2 = "testUser2";
+ IterableIdentityResolution identityResolution2 = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setUserId(userId2, identityResolution2);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), mergeRequest.getPath());
+
+ // check that user id was set
+ assertEquals(userId2, IterableApi.getInstance().getUserId());
+ }
+
+ @Test
+ public void testIdentifiedUserIdMergeTrue() throws Exception {
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set user id
+ final String userId1 = "testUser1";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableApi.getInstance().setUserId(userId1, identityResolution);
+
+ // check that user id was set
+ assertEquals(userId1, IterableApi.getInstance().getUserId());
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set different user id
+ final String userId2 = "testUser2";
+ IterableIdentityResolution identityResolution2 = new IterableIdentityResolution(true, true);
+ IterableApi.getInstance().setUserId(userId2, identityResolution2);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());
+
+ // check that user id was set
+ assertEquals(userId2, IterableApi.getInstance().getUserId());
+ }
+
+ // all email tests
+ @Test
+ public void testCriteriaNotMetEmailDefault() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock merge response
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email = "testUser2@gmail.com";
+ IterableApi.getInstance().setEmail(email);
+
+ // check that request was sent to purchase endpoint on event replay
+ RecordedRequest purchaseRequest2 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(purchaseRequest2);
+ assertEquals(("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE), purchaseRequest2.getPath());
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest consentRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(consentRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), consentRequest.getPath());
+
+
+ // check that email was set
+ assertEquals(email, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testCriteriaNotMetEmailReplayTrueMergeFalse() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock merge response
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email = "testUser@gmail.com";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setEmail(email, identityResolution);
+
+ // check that request was sent to purchase endpoint on event replay
+ RecordedRequest purchaseRequest2 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(purchaseRequest2);
+ assertEquals(("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE), purchaseRequest2.getPath());
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest consentRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(consentRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), consentRequest.getPath());
+
+ // check that email was set
+ assertEquals(email, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testCriteriaNotMetEmailReplayFalseMergeFalse() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock merge response
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email = "testUser@gmail.com";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(false, false);
+ IterableApi.getInstance().setEmail(email, identityResolution);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), mergeRequest.getPath());
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_TRACK_CONSENT), mergeRequest.getPath());
+
+ // check that email was set
+ assertEquals(email, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testCriteriaNotMetEmailReplayFalseMergeTrue() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 5, 1);
+ shadowOf(getMainLooper()).idle();
+
+ // check that request was not sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be an unknown user session request", unknownSessionRequest);
+
+ // check that request was not sent to track purchase endpoint
+ RecordedRequest purchaseRequest1 = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNull("There should not be a purchase request", purchaseRequest1);
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock merge response
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email = "testUser@gmail.com";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(false, true);
+ IterableApi.getInstance().setEmail(email, identityResolution);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(request);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), request.getPath());
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_TRACK_CONSENT), request.getPath());
+
+ // check that email was set
+ assertEquals(email, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testCriteriaMetEmailDefault() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+
+ // check if request was sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());
+
+ // check if request was sent to track purchase endpoint
+ RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", purchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath());
+
+ // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
+ RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("InApp messages request should be sent", inAppRequest);
+ assertTrue("InApp messages request path should start with correct endpoint",
+ inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock merge response
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email to trigger merging
+ final String email = "testUser2@gmail.com";
+ IterableApi.getInstance().setEmail(email);
+
+ // check if request was sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());
+
+ // check that email was set
+ assertEquals(email, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testCriteriaMetEmailMergeFalse() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+
+ // check if request was sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());
+
+ // check if request was sent to track purchase endpoint
+ RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", purchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath());
+
+ // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
+ RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("InApp messages request should be sent", inAppRequest);
+ assertTrue("InApp messages request path should start with correct endpoint",
+ inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock merge response
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email = "testUser@gmail.com";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setEmail(email, identityResolution);
+
+ // check if request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), mergeRequest.getPath());
+
+ // check that email was set
+ assertEquals(email, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testCriteriaMetEmailMergeTrue() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+
+ // check if request was sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());
+
+ // check if request was sent to track purchase endpoint
+ RecordedRequest purchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", purchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, purchaseRequest.getPath());
+
+ // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
+ RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("InApp messages request should be sent", inAppRequest);
+ assertTrue("InApp messages request path should start with correct endpoint",
+ inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock merge response
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email = "testUser@gmail.com";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableApi.getInstance().setEmail(email, identityResolution);
+
+ // check if request was sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());
+
+ // check that email was set
+ assertEquals(email, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testIdentifiedEmailDefault() throws Exception {
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email1 = "testUser1@gmail.com";
+ IterableApi.getInstance().setEmail(email1);
+
+ // check that email was set
+ assertEquals(email1, IterableApi.getInstance().getEmail());
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set different email
+ final String email2 = "testUser2@gmail.com";
+ IterableApi.getInstance().setEmail(email2);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), mergeRequest.getPath());
+
+ // check that email was set
+ assertEquals(email2, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testIdentifiedEmailMergeFalse() throws Exception {
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email1 = "testUser1@gmail.com";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, false);
+ IterableApi.getInstance().setEmail(email1, identityResolution);
+
+ // check that email was set
+ assertEquals(email1, IterableApi.getInstance().getEmail());
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set different email
+ final String email2 = "testUser2@gmail.com";
+ IterableApi.getInstance().setEmail(email2, identityResolution);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals(("/" + IterableConstants.ENDPOINT_MERGE_USER), mergeRequest.getPath());
+
+ // check that email was set
+ assertEquals(email2, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testIdentifiedEmailMergeTrue() throws Exception {
+ addResponse(IterableConstants.ENDPOINT_MERGE_USER);
+
+ // set email
+ final String email1 = "testUser1@gmail.com";
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableApi.getInstance().setEmail(email1, identityResolution);
+
+ // check that email was set
+ assertEquals(email1, IterableApi.getInstance().getEmail());
+
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // set different email
+ final String email2 = "testUser2@gmail.com";
+ IterableApi.getInstance().setEmail(email2, identityResolution);
+
+ // check that request was not sent to merge endpoint
+ RecordedRequest mergeRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull(mergeRequest);
+ assertNotEquals("/" + IterableConstants.ENDPOINT_MERGE_USER, mergeRequest.getPath());
+
+ // check that email was set
+ assertEquals(email2, IterableApi.getInstance().getEmail());
+ }
+
+ @Test
+ public void testCriteriaMetTwice() throws Exception {
+ // clear any pending requests
+ while (server.takeRequest(1, TimeUnit.SECONDS) != null) { }
+
+ // mock unknown user session response and track purchase response
+ addResponse(IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_TRACK_PURCHASE);
+ addResponse(IterableConstants.ENDPOINT_GET_INAPP_MESSAGES);
+
+ // trigger track purchase event
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ triggerTrackPurchaseEvent("test", "keyboard", 4.67, 3);
+ shadowOf(getMainLooper()).idle();
+
+ // check if only one request was sent to unknown user session endpoint
+ RecordedRequest unknownSessionRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Unknown user session request should not be null", unknownSessionRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_UNKNOWN_SESSION, unknownSessionRequest.getPath());
+
+ // check if first request was sent to track purchase endpoint
+ RecordedRequest firstPurchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", firstPurchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, firstPurchaseRequest .getPath());
+
+ // check if second request was sent to track purchase endpoint
+ RecordedRequest secondPurchaseRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("Purchase request should not be null", secondPurchaseRequest);
+ assertEquals("/" + IterableConstants.ENDPOINT_TRACK_PURCHASE, secondPurchaseRequest.getPath());
+
+ // check if request was sent to getInAppMessages endpoint (triggered by completeUserLogin)
+ RecordedRequest inAppRequest = server.takeRequest(1, TimeUnit.SECONDS);
+ assertNotNull("InApp messages request should be sent", inAppRequest);
+ assertTrue("InApp messages request path should start with correct endpoint",
+ inAppRequest.getPath().startsWith("/" + IterableConstants.ENDPOINT_GET_INAPP_MESSAGES));
+ }
+}
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java
index 14e0ab373..bb781b9ad 100644
--- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiRequestTest.java
@@ -256,4 +256,94 @@ public void testUpdateEmailRequest() throws Exception {
assertNotNull(request3);
Assert.assertEquals("{\"currentEmail\":\"test@example.com\",\"newEmail\":\"another@email.com\"}", request3.getBody().readUtf8());
}
+
+ @Test
+ public void testTrackConsentWithUserId() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
+
+ // Test track consent with userId
+ long timestamp = 1234567890L;
+ IterableApi.sharedInstance.apiClient.trackConsent("testUser123", null, timestamp, true);
+
+ RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
+ assertNotNull(request);
+ Assert.assertEquals("/" + IterableConstants.ENDPOINT_TRACK_CONSENT, request.getPath());
+
+ JSONObject requestJson = new JSONObject(request.getBody().readUtf8());
+ assertTrue("Request should contain userId", requestJson.has("userId"));
+ assertTrue("Request should contain consentTimestamp", requestJson.has("consentTimestamp"));
+ assertTrue("Request should contain isUserKnown", requestJson.has("isUserKnown"));
+ assertTrue("Request should contain deviceInfo", requestJson.has("deviceInfo"));
+ assertFalse("Request should not contain email", requestJson.has("email"));
+
+ Assert.assertEquals("testUser123", requestJson.getString("userId"));
+ Assert.assertEquals(timestamp, requestJson.getLong("consentTimestamp"));
+ assertTrue("isUserKnown should be true", requestJson.getBoolean("isUserKnown"));
+ }
+
+ @Test
+ public void testTrackConsentWithEmail() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
+
+ // Test track consent with email
+ long timestamp = 9876543210L;
+ IterableApi.sharedInstance.apiClient.trackConsent(null, "test@example.com", timestamp, false);
+
+ RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
+ assertNotNull(request);
+ Assert.assertEquals("/" + IterableConstants.ENDPOINT_TRACK_CONSENT, request.getPath());
+
+ JSONObject requestJson = new JSONObject(request.getBody().readUtf8());
+ assertTrue("Request should contain email", requestJson.has("email"));
+ assertTrue("Request should contain consentTimestamp", requestJson.has("consentTimestamp"));
+ assertTrue("Request should contain isUserKnown", requestJson.has("isUserKnown"));
+ assertTrue("Request should contain deviceInfo", requestJson.has("deviceInfo"));
+ assertFalse("Request should not contain userId", requestJson.has("userId"));
+
+ Assert.assertEquals("test@example.com", requestJson.getString("email"));
+ Assert.assertEquals(timestamp, requestJson.getLong("consentTimestamp"));
+ assertFalse("isUserKnown should be false", requestJson.getBoolean("isUserKnown"));
+ }
+
+ @Test
+ public void testTrackConsentWithBothUserIdAndEmail() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
+
+ // Test track consent with both userId and email
+ long timestamp = 1111111111L;
+ IterableApi.sharedInstance.apiClient.trackConsent("user456", "user@test.com", timestamp, true);
+
+ RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
+ assertNotNull(request);
+ Assert.assertEquals("/" + IterableConstants.ENDPOINT_TRACK_CONSENT, request.getPath());
+
+ JSONObject requestJson = new JSONObject(request.getBody().readUtf8());
+ assertTrue("Request should contain userId", requestJson.has("userId"));
+ assertTrue("Request should contain email", requestJson.has("email"));
+ assertTrue("Request should contain consentTimestamp", requestJson.has("consentTimestamp"));
+ assertTrue("Request should contain isUserKnown", requestJson.has("isUserKnown"));
+ assertTrue("Request should contain deviceInfo", requestJson.has("deviceInfo"));
+
+ Assert.assertEquals("user456", requestJson.getString("userId"));
+ Assert.assertEquals("user@test.com", requestJson.getString("email"));
+ Assert.assertEquals(timestamp, requestJson.getLong("consentTimestamp"));
+ assertTrue("isUserKnown should be true", requestJson.getBoolean("isUserKnown"));
+ }
+
+ @Test
+ public void testTrackConsentRequestHeaders() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
+
+ // Test track consent headers
+ long timestamp = 2222222222L;
+ IterableApi.sharedInstance.apiClient.trackConsent("headerTestUser", null, timestamp, false);
+
+ RecordedRequest request = server.takeRequest(5, TimeUnit.SECONDS);
+ assertNotNull(request);
+ Assert.assertEquals("/" + IterableConstants.ENDPOINT_TRACK_CONSENT, request.getPath());
+ Assert.assertEquals("Android", request.getHeader(IterableConstants.HEADER_SDK_PLATFORM));
+ Assert.assertEquals(IterableConstants.ITBL_KEY_SDK_VERSION_NUMBER, request.getHeader(IterableConstants.HEADER_SDK_VERSION));
+ Assert.assertEquals("fake_key", request.getHeader(IterableConstants.HEADER_API_KEY));
+ assertNotNull("sentAt header should be present", request.getHeader(IterableConstants.KEY_SENT_AT));
+ }
}
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java
index 5d4ec5ab5..463cf2b7c 100644
--- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableApiTest.java
@@ -2,6 +2,8 @@
import com.iterable.iterableapi.util.DeviceInfoUtils;
import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
import android.net.Uri;
import androidx.annotation.NonNull;
@@ -173,7 +175,7 @@ public void testSetEmailWithCallback() {
IterableApi.initialize(getContext(), "apiKey");
String email = "test@example.com";
- IterableApi.getInstance().setEmail(email, null, new IterableHelper.SuccessHandler() {
+ IterableApi.getInstance().setEmail(email, new IterableHelper.SuccessHandler() {
@Override
public void onSuccess(@NonNull JSONObject data) {
assertTrue(true); // callback should be called with success
@@ -191,7 +193,7 @@ public void testSetUserIdWithCallback() {
IterableApi.initialize(getContext(), "apiKey");
String userId = "test_user_id";
- IterableApi.getInstance().setUserId(userId, null, new IterableHelper.SuccessHandler() {
+ IterableApi.getInstance().setUserId(userId, new IterableHelper.SuccessHandler() {
@Override
public void onSuccess(@NonNull JSONObject data) {
assertTrue(true); // callback should be called with success
@@ -866,4 +868,335 @@ public void testFetchRemoteConfigurationCalledWhenInForeground() throws Exceptio
IterableActivityMonitor.instance = new IterableActivityMonitor();
}
+ //region Consent Logging Tests - Direct Unit Tests
+ //---------------------------------------------------------------------------------------
+
+ @Test
+ public void testTrackConsentForUser_WithValidConditions() throws Exception {
+ // Setup: Configure for consent logging
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setIdentityResolution(identityResolution)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+
+ // Set up conditions for consent logging
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+ IterableApi.getInstance().setEmail("test@example.com");
+
+ // Mock the API client to verify trackConsent is called
+ IterableApiClient mockClient = mock(IterableApiClient.class);
+ IterableApi.getInstance().apiClient = mockClient;
+
+ // Execute: Call trackConsentForUser directly
+ IterableApi.getInstance().trackConsentForUser("test@example.com", null, true);
+
+ // Verify: trackConsent was called on the API client
+ verify(mockClient).trackConsent(
+ nullable(String.class),
+ eq("test@example.com"),
+ any(Long.class),
+ eq(true)
+ );
+ }
+
+ @Test
+ public void testTrackConsentForUser_DoesNotCallWhenUnknownUserActivationDisabled() throws Exception {
+ // Setup: Configure WITHOUT unknown user activation
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(false) // Disabled!
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+
+ // Set up other conditions
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+ IterableApi.getInstance().setEmail("test@example.com");
+
+ // Mock the API client
+ IterableApiClient mockClient = mock(IterableApiClient.class);
+ IterableApi.getInstance().apiClient = mockClient;
+
+ // Execute: Call trackConsentForUser
+ IterableApi.getInstance().trackConsentForUser("test@example.com", null, true);
+
+ // Verify: trackConsent was NOT called (unknown user activation disabled)
+ verify(mockClient, never()).trackConsent(any(), any(), any(Long.class), any(Boolean.class));
+ }
+
+ @Test
+ public void testTrackConsentForUser_DoesNotCallWhenVisitorUsageNotTracked() throws Exception {
+ // Setup: Configure for consent logging
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+
+ // Set visitor usage NOT tracked
+ IterableApi.getInstance().setVisitorUsageTracked(false);
+ IterableApi.getInstance().setEmail("test@example.com");
+
+ // Mock the API client
+ IterableApiClient mockClient = mock(IterableApiClient.class);
+ IterableApi.getInstance().apiClient = mockClient;
+
+ // Execute: Call trackConsentForUser
+ IterableApi.getInstance().trackConsentForUser("test@example.com", null, true);
+
+ // Verify: trackConsent was NOT called (visitor usage not tracked)
+ verify(mockClient, never()).trackConsent(any(), any(), any(Long.class), any(Boolean.class));
+ }
+
+ @Test
+ public void testConsentLoggingConditions_AllTrue() throws Exception {
+ // Setup: All conditions should be true
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setIdentityResolution(identityResolution)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+
+ // Clear consent logged flag
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.remove(IterableConstants.SHARED_PREFS_CONSENT_LOGGED);
+ editor.apply();
+
+ IterableApi api = IterableApi.getInstance();
+
+ // Verify all conditions are met
+ assertTrue("enableUnknownUserActivation should be true", api.config.enableUnknownUserActivation);
+ assertTrue("getVisitorUsageTracked should be true", api.getVisitorUsageTracked());
+ assertTrue("getReplayOnVisitorToKnown should be true", api.config.identityResolution.getReplayOnVisitorToKnown());
+ assertFalse("getConsentLogged should be false",
+ sharedPref.getBoolean(IterableConstants.SHARED_PREFS_CONSENT_LOGGED, false));
+ }
+
+ @Test
+ public void testConsentLoggingConditions_ConsentAlreadyLogged() throws Exception {
+ // Setup
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setIdentityResolution(identityResolution)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+
+ // Set consent already logged
+ SharedPreferences sharedPref = getContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ editor.putBoolean(IterableConstants.SHARED_PREFS_CONSENT_LOGGED, true);
+ editor.apply();
+
+ // Verify consent is already logged
+ assertTrue("getConsentLogged should be true",
+ sharedPref.getBoolean(IterableConstants.SHARED_PREFS_CONSENT_LOGGED, false));
+ }
+
+ @Test
+ public void testRegisterDeviceTokenSuccessCallback_CreatesWrappedHandler() throws Exception {
+ // Setup
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setIdentityResolution(identityResolution)
+ .setAutoPushRegistration(false)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+
+ // Create a mock success handler
+ IterableHelper.SuccessHandler originalHandler = mock(IterableHelper.SuccessHandler.class);
+
+ // Set up user with success handler
+ IterableApi.getInstance().setEmail("test@example.com", originalHandler, null);
+
+ // Spy on the API client to capture the success handler passed to registerDeviceToken
+ IterableApiClient mockClient = spy(IterableApi.getInstance().apiClient);
+ IterableApi.getInstance().apiClient = mockClient;
+
+ // Execute: Call registerDeviceToken
+ IterableApi.getInstance().registerDeviceToken("test_token");
+
+ // Wait for async thread
+ Thread.sleep(100);
+ shadowOf(getMainLooper()).idle();
+
+ // Verify: registerDeviceToken was called with a success handler
+ ArgumentCaptor successCaptor = ArgumentCaptor.forClass(IterableHelper.SuccessHandler.class);
+ verify(mockClient, timeout(1000)).registerDeviceToken(
+ eq("test@example.com"),
+ nullable(String.class),
+ nullable(String.class),
+ any(String.class),
+ eq("test_token"),
+ nullable(JSONObject.class),
+ any(),
+ successCaptor.capture(),
+ nullable(IterableHelper.FailureHandler.class)
+ );
+
+ // The captured handler should not be null (a wrapper was created)
+ assertNotNull("Success handler should not be null", successCaptor.getValue());
+ }
+
+ @Test
+ public void testRegisterDeviceTokenSuccessCallback_WithoutOriginalHandler() throws Exception {
+ // Setup
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setIdentityResolution(identityResolution)
+ .setAutoPushRegistration(false)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+ IterableApi.getInstance().setUserId("test_user_123");
+
+ // Spy on the API client to capture the success handler passed to registerDeviceToken
+ IterableApiClient mockClient = spy(IterableApi.getInstance().apiClient);
+ IterableApi.getInstance().apiClient = mockClient;
+
+ // Execute: Call registerDeviceToken without setting an explicit success handler
+ IterableApi.getInstance().registerDeviceToken("test_token");
+
+ // Wait for async thread
+ Thread.sleep(100);
+ shadowOf(getMainLooper()).idle();
+
+ // Verify: registerDeviceToken was called with a success handler (the wrapper)
+ ArgumentCaptor successCaptor = ArgumentCaptor.forClass(IterableHelper.SuccessHandler.class);
+ verify(mockClient, timeout(1000)).registerDeviceToken(
+ nullable(String.class),
+ eq("test_user_123"),
+ nullable(String.class),
+ any(String.class),
+ eq("test_token"),
+ nullable(JSONObject.class),
+ any(),
+ successCaptor.capture(),
+ nullable(IterableHelper.FailureHandler.class)
+ );
+
+ // The captured handler should not be null (wrapper was created for consent logging)
+ assertNotNull("Success handler should not be null", successCaptor.getValue());
+ }
+
+ @Test
+ public void testRegisterDeviceTokenIntegration_ConsentLoggingTriggered() throws Exception {
+ // Setup: Mock server responses
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); // for registerDeviceToken
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); // for trackConsent
+
+ // Setup: Configure for consent logging
+ IterableIdentityResolution identityResolution = new IterableIdentityResolution(true, true);
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(true)
+ .setIdentityResolution(identityResolution)
+ .setAutoPushRegistration(false)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+
+ // Set up conditions for consent logging
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+
+ // Create a success handler and set user
+ IterableHelper.SuccessHandler successHandler = mock(IterableHelper.SuccessHandler.class);
+ IterableApi.getInstance().setEmail("test@example.com", successHandler, null);
+
+ // Execute: Register device token
+ IterableApi.getInstance().registerDeviceToken("test_token");
+
+ // Wait for async operations
+ Thread.sleep(200);
+ shadowOf(getMainLooper()).idle();
+
+ // Verify: At least 2 requests were made (registerDeviceToken + consent, potentially others for auth)
+ assertTrue("Should have made at least 2 requests", server.getRequestCount() >= 2);
+
+ // Verify: consent request was made (check all requests for it)
+ boolean foundRegisterRequest = false;
+ boolean foundConsentRequest = false;
+
+ for (int i = 0; i < server.getRequestCount(); i++) {
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ if (request != null) {
+ if (request.getPath().contains("registerDeviceToken")) {
+ foundRegisterRequest = true;
+ } else if (request.getPath().contains("unknownuser/consent")) {
+ foundConsentRequest = true;
+ }
+ }
+ }
+
+ assertTrue("Should have made registerDeviceToken request", foundRegisterRequest);
+ assertTrue("Should have made consent request", foundConsentRequest);
+
+ // Verify: Original success handler was called at least once
+ verify(successHandler, atLeastOnce()).onSuccess(any(JSONObject.class));
+ }
+
+ @Test
+ public void testRegisterDeviceTokenIntegration_ConsentLoggingNotTriggeredWhenDisabled() throws Exception {
+ // Setup: Mock server response
+ server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); // for registerDeviceToken only
+
+ // Setup: Configure WITHOUT consent logging (unknown user activation disabled)
+ IterableConfig config = new IterableConfig.Builder()
+ .setEnableUnknownUserActivation(false) // Disabled
+ .setAutoPushRegistration(false)
+ .build();
+
+ IterableApi.initialize(getContext(), "apiKey", config);
+
+ // Set up other conditions
+ IterableApi.getInstance().setVisitorUsageTracked(true);
+ IterableHelper.SuccessHandler successHandler = mock(IterableHelper.SuccessHandler.class);
+ IterableApi.getInstance().setEmail("test@example.com", successHandler, null);
+
+ // Execute: Register device token
+ IterableApi.getInstance().registerDeviceToken("test_token");
+
+ // Wait for async operations
+ Thread.sleep(200);
+ shadowOf(getMainLooper()).idle();
+
+ // Verify: At least one request was made but no consent
+ assertTrue("Should have made at least 1 request", server.getRequestCount() >= 1);
+
+ // Verify: No consent request was made (check all requests)
+ boolean foundRegisterRequest = false;
+ boolean foundConsentRequest = false;
+
+ for (int i = 0; i < server.getRequestCount(); i++) {
+ RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS);
+ if (request != null) {
+ if (request.getPath().contains("registerDeviceToken")) {
+ foundRegisterRequest = true;
+ } else if (request.getPath().contains("unknownuser/consent")) {
+ foundConsentRequest = true;
+ }
+ }
+ }
+
+ assertTrue("Should have made registerDeviceToken request", foundRegisterRequest);
+ assertFalse("Should NOT have made consent request", foundConsentRequest);
+
+ // Verify: Original success handler was called at least once
+ verify(successHandler, atLeastOnce()).onSuccess(any(JSONObject.class));
+ }
+
+ //endregion
+
}
diff --git a/iterableapi/src/test/java/com/iterable/iterableapi/IterableUtilRule.java b/iterableapi/src/test/java/com/iterable/iterableapi/IterableUtilRule.java
index 5d17ff967..4ff34bf3b 100644
--- a/iterableapi/src/test/java/com/iterable/iterableapi/IterableUtilRule.java
+++ b/iterableapi/src/test/java/com/iterable/iterableapi/IterableUtilRule.java
@@ -1,10 +1,10 @@
package com.iterable.iterableapi;
+import static org.mockito.Mockito.spy;
+
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
-import static org.mockito.Mockito.spy;
-
public class IterableUtilRule extends TestWatcher {
private IterableUtilImpl originalIterableUtil;
public IterableUtilImpl iterableUtilSpy;