From d6c05c70c2818aca34762835033884264274f6cd Mon Sep 17 00:00:00 2001 From: hani Date: Mon, 18 Dec 2023 18:03:55 +0530 Subject: [PATCH 001/248] Implement Anonymous user tracking --- AnonymousUserEventTracking.md | 124 ++++++ iterableapi/build.gradle | 1 + .../util/CriteriaCompletionCheckerTest.java | 251 ++++++++++++ .../iterableapi/AnonymousUserManager.java | 386 ++++++++++++++++++ .../iterableapi/AnonymousUserMerge.java | 56 +++ .../com/iterable/iterableapi/IterableApi.java | 47 ++- .../iterableapi/IterableApiClient.java | 106 +++++ .../iterableapi/IterableConstants.java | 27 ++ .../iterable/iterableapi/ddl/Criteria.java | 55 +++ .../util/CriteriaCompletionChecker.java | 331 +++++++++++++++ 10 files changed, 1383 insertions(+), 1 deletion(-) create mode 100644 AnonymousUserEventTracking.md create mode 100644 iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java create mode 100644 iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java create mode 100644 iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java create mode 100644 iterableapi/src/main/java/com/iterable/iterableapi/ddl/Criteria.java create mode 100644 iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java diff --git a/AnonymousUserEventTracking.md b/AnonymousUserEventTracking.md new file mode 100644 index 000000000..c1f851036 --- /dev/null +++ b/AnonymousUserEventTracking.md @@ -0,0 +1,124 @@ +# AnonymousUserManager Class + +## Class Introduction + +The `AnonymousUserManager` class is responsible for managing anonymous user sessions and tracking events in an Android application. +It includes methods for updating sessions, tracking events, and handling criteria for transitioning to known users. + +## Class Structure + +The `AnonymousUserManager` class includes the following key components: + +- **Fields:** + - `TAG`: A constant String for logging. + +- **Methods:** + - `updateAnonSession()`: Updates the anonymous user session. + - `trackAnonEvent(String eventName, JSONObject dataFields)`: Tracks an anonymous event. + - `trackAnonPurchaseEvent(double total, List items, JSONObject dataFields)`: Tracks an anonymous purchase event. + - `trackAnonUpdateCart(List items)`: Tracks an anonymous cart update. + - `getCriteria()`: Retrieves criteria for user transition. + - `checkCriteriaCompletion()`: Checks if criteria for user transition are met. + - `createKnownUser()`: Creates a known user after criteria are met. + - `syncEvents()`: Syncs tracked events with the server. + - `clearLocallyStoredData()`: Clears locally stored data. + - `storeEventListToLocalStorage(JSONObject newDataObject)`: Stores event data in local storage. + - `getEventListFromLocalStorage()`: Retrieves event data from local storage. + - `getCurrentTime()`: Gets the current time in milliseconds. + - `getCurrentDateTime()`: Gets the current date and time in UTC format. + +## Methods Description + +### `updateAnonSession()` + +This method updates the anonymous user session. It does the following: + +* Retrieves the previous session data from SharedPreferences. +* Increments the session number. +* Stores the updated session data in SharedPreferences. + +### `trackAnonEvent(eventName, dataFields)` + +This method tracks an anonymous event. It does the following: + +* Creates a JSON object with event details, including the event name, timestamp, data fields, and tracking type. +* Stores the event data in local storage. +* Checks criteria completion and creates a known user if criteria are met. + +### `trackAnonPurchaseEvent(total, items, dataFields)` + +This method tracks an anonymous purchase event. It does the following: + +* Converts the list of commerce items to JSON. +* Creates a JSON object with purchase event details, including items, total, timestamp, data fields, and tracking type. +* Stores the purchase event data in local storage. +* Checks criteria completion and creates a known user if criteria are met. + +### `trackAnonUpdateCart(items)` + +This method tracks an anonymous cart update. It does the following: + +* Converts the list of commerce items to JSON. +* Creates a JSON object with cart update details, including items, timestamp, and tracking type. +* Stores the cart update data in local storage. +* Checks criteria completion and creates a known user if criteria are met. + +### `getCriteria()` + +This method is responsible for fetching criteria data. It simulates calling an API and saving data in SharedPreferences. + +### `checkCriteriaCompletion()` + +This private method checks if criteria for creating a known user are met. It compares stored event data with predefined criteria and returns `true` if criteria are completed. + +### `createKnownUser()` + +This method is responsible for creating a known user in the Iterable API. It does the following: + +* Sets a random user ID using a UUID (Universally Unique Identifier). +* Retrieves user session data from SharedPreferences. +* If user session data exists, it updates the user information in the Iterable API. +* Calls the syncEvents() method to synchronize tracked events. +* Finally, it clears locally stored data using the clearLocallyStoredData() method. + +### `syncEvents()` + +This method is used to synchronize tracked events stored in local storage with the Iterable API. It performs the following tasks: + +* Retrieves the list of tracked events from local storage using the getEventListFromLocalStorage() method. +* Iterates through the list of events and processes each event based on its type. +* Supported event types include regular event tracking, purchase event tracking, and cart update tracking. +* For each event, it extracts relevant data, including event name, data fields, items (for purchase and cart update events), and timestamps. +* It uses the Iterable API to track these events or update the user's cart. +* After processing all events, it calls the clearLocallyStoredData() method to clear locally stored event data. + +### `clearLocallyStoredData()` + +This method is responsible for clearing locally stored data in SharedPreferences. It removes both the anonymous session data and the event list data, effectively cleaning up after successful synchronization with the Iterable API. + +### `storeEventListToLocalStorage(JSONObject newDataObject)` + +This method is used to add a new event to the list of events stored in local storage. It takes a JSON object representing the new event data and performs the following steps: + +* Retrieves the existing list of events from local storage using the getEventListFromLocalStorage() method. +* Appends the new event data to the list. +* Updates the event list data in SharedPreferences. + +### `getEventListFromLocalStorage()` + +This method retrieves the list of tracked events from local storage in SharedPreferences. It returns a JSONArray containing the stored event data. If no data is found or an error occurs during retrieval, it returns an empty JSONArray. + +### `getCurrentTime()` + +This method returns the current timestamp as a long value, representing the number of milliseconds + +## Usage + +You can use the `AnonymousUserManager` class to manage anonymous user sessions, track events, and determine when to create known users based on predefined criteria. + +```java +// Example usage +AnonymousUserManager userManager = new AnonymousUserManager(); +userManager.updateAnonSession(); +userManager.trackAnonEvent("UserLoggedIn", userData); +userManager.trackAnonPurchaseEvent(100.0, items, otherData); \ No newline at end of file diff --git a/iterableapi/build.gradle b/iterableapi/build.gradle index 40abefab6..5f43d7d49 100644 --- a/iterableapi/build.gradle +++ b/iterableapi/build.gradle @@ -57,6 +57,7 @@ dependencies { api 'androidx.appcompat:appcompat:1.0.0' api 'androidx.annotation:annotation:1.0.0' api 'com.google.firebase:firebase-messaging:20.3.0' + implementation 'com.google.code.gson:gson:2.10.1' implementation "androidx.security:security-crypto:1.1.0-alpha06" testImplementation 'junit:junit:4.13.2' 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..f36d84851 --- /dev/null +++ b/iterableapi/src/androidTest/java/com/iterable/iterableapi/util/CriteriaCompletionCheckerTest.java @@ -0,0 +1,251 @@ +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" + + " \"criteriaList\":[\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" + + " \"criteriaList\":[\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" + + "}"; + + @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,\"dataType\":\"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\\\":3.5,\\\"quantity\\\":3}]\",\"createdAt\":1700071052507,\"total\":4.67,\"dataType\":\"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); + } +} \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java new file mode 100644 index 000000000..5df2a9882 --- /dev/null +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java @@ -0,0 +1,386 @@ +package com.iterable.iterableapi; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.UUID; + +public class AnonymousUserManager { + + private static final String TAG = "RNIterableAPIModule"; + + void updateAnonSession() { + IterableLogger.v(TAG, "updateAnonSession"); + SharedPreferences sharedPref = IterableApi.sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + String previousData = sharedPref.getString(IterableConstants.SHARED_PREFS_ANON_SESSIONS, ""); + + try { + int sessionNo = 0; + String firstSessionDate = ""; + + if (!previousData.isEmpty()) { + JSONObject previousDataJson = new JSONObject(previousData); + JSONObject sessionObject = previousDataJson.getJSONObject(IterableConstants.SHARED_PREFS_ANON_SESSIONS); + sessionNo = sessionObject.getInt(IterableConstants.SHARED_PREFS_SESSION_NO); + firstSessionDate = sessionObject.getString(IterableConstants.SHARED_PREFS_FIRST_SESSION); + } + + JSONObject newDataObject = new JSONObject(); + newDataObject.put(IterableConstants.SHARED_PREFS_SESSION_NO, sessionNo + 1); + newDataObject.put(IterableConstants.SHARED_PREFS_LAST_SESSION, getCurrentDateTime()); + + if (firstSessionDate.isEmpty()) { + newDataObject.put(IterableConstants.SHARED_PREFS_FIRST_SESSION, getCurrentDateTime()); + } else { + newDataObject.put(IterableConstants.SHARED_PREFS_FIRST_SESSION, firstSessionDate); + } + + JSONObject anonSessionData = new JSONObject(); + anonSessionData.put(IterableConstants.SHARED_PREFS_ANON_SESSIONS, newDataObject); + + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(IterableConstants.SHARED_PREFS_ANON_SESSIONS, anonSessionData.toString()); + editor.apply(); + + } catch (JSONException e) { + e.printStackTrace(); + } + } + + void trackAnonEvent(String eventName, JSONObject dataFields) { + IterableLogger.v(TAG, "trackAnonEvent"); + + try { + JSONObject newDataObject = new JSONObject(); + newDataObject.put(IterableConstants.KEY_EVENT_NAME, eventName); + newDataObject.put(IterableConstants.KEY_CREATED_AT, getCurrentTime()); + 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 trackAnonPurchaseEvent(double total, @NonNull List items, @Nullable JSONObject dataFields) { + + IterableLogger.v(TAG, "trackAnonPurchaseEvent"); + 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, getCurrentTime()); + 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 trackAnonUpdateCart(@NonNull List items) { + + IterableLogger.v(TAG, "trackAnonUpdateCart"); + + 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, getCurrentTime()); + storeEventListToLocalStorage(newDataObject); + + } catch (JSONException e) { + e.printStackTrace(); + } + } + + void getCriteria() { + try { + // call API when it is available and save data in SharedPreferences, until then just save the data using static data + String mockData = "{" + + " \"count\":2," + + " \"criteriaList\":[" + + " {" + + " \"criteriaId\":12345," + + " \"searchQuery\":{" + + " \"combinator\":\"Or\"," + + " \"searchQueries\":[" + + " {" + + " \"combinator\":\"And\"," + + " \"searchQueries\":[" + + " {" + + " \"dataType\":\"purchase\"," + + " \"searchCombo\":{" + + " \"combinator\":\"And\"," + + " \"searchQueries\":[" + + " {" + + " \"field\":\"shoppingCartItems.price\"," + + " \"fieldType\":\"double\"," + + " \"comparatorType\":\"Equals\"," + + " \"dataType\":\"purchase\"," + + " \"id\":2," + + " \"value\":\"4.67\"" + + " }," + + " {" + + " \"field\":\"shoppingCartItems.quantity\"," + + " \"fieldType\":\"long\"," + + " \"comparatorType\":\"GreaterThanOrEqualTo\"," + + " \"dataType\":\"purchase\"," + + " \"id\":3," + + " \"valueLong\":2," + + " \"value\":\"2\"" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + " }" + + " ]" + + " }" + + " }," + + " {" + + " \"criteriaId\":5678," + + " \"searchQuery\":{" + + " \"combinator\":\"Or\"," + + " \"searchQueries\":[" + + " {" + + " \"combinator\":\"Or\"," + + " \"searchQueries\":[" + + " {" + + " \"dataType\":\"user\"," + + " \"searchCombo\":{" + + " \"combinator\":\"And\"," + + " \"searchQueries\":[" + + " {" + + " \"field\":\"itblInternal.emailDomain\"," + + " \"fieldType\":\"string\"," + + " \"comparatorType\":\"Equals\"," + + " \"dataType\":\"user\"," + + " \"id\":6," + + " \"value\":\"gmail.com\"" + + " }" + + " ]" + + " }" + + " }," + + " {" + + " \"dataType\":\"customEvent\"," + + " \"searchCombo\":{" + + " \"combinator\":\"And\"," + + " \"searchQueries\":[" + + " {" + + " \"field\":\"eventName\"," + + " \"fieldType\":\"string\"," + + " \"comparatorType\":\"Equals\"," + + " \"dataType\":\"customEvent\"," + + " \"id\":9," + + " \"value\":\"processing_cancelled\"" + + " }," + + " {" + + " \"field\":\"createdAt\"," + + " \"fieldType\":\"date\"," + + " \"comparatorType\":\"GreaterThan\"," + + " \"dataType\":\"customEvent\"," + + " \"id\":10," + + " \"dateRange\":{" + + " }," + + " \"isRelativeDate\":false," + + " \"value\":\"1688194800000\"" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + " }" + + " ]" + + " }" + + " }" + + " ]" + + "}"; + + JSONObject mockDataObject = new JSONObject(mockData); + SharedPreferences sharedPref = IterableApi.sharedInstance.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 boolean checkCriteriaCompletion() { + SharedPreferences sharedPref = IterableApi.sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + String criteriaData = sharedPref.getString(IterableConstants.SHARED_PREFS_CRITERIA, ""); + JSONArray localStoredEventList = getEventListFromLocalStorage(); + + try { + if (!criteriaData.isEmpty() && localStoredEventList.length() > 0) { + CriteriaCompletionChecker checker = new CriteriaCompletionChecker(); + return checker.getMatchedCriteria(criteriaData, localStoredEventList) != null; + } + } catch (Exception e) { + e.printStackTrace(); + } + + return false; + } + + private void createKnownUser() { + IterableApi.getInstance().setUserId(UUID.randomUUID().toString()); + SharedPreferences sharedPref = IterableApi.sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + String userData = sharedPref.getString(IterableConstants.SHARED_PREFS_ANON_SESSIONS, ""); + + try { + if (!userData.isEmpty()) { + JSONObject userDataJson = new JSONObject(userData); + IterableApi.getInstance().updateUser(userDataJson); + } + + } catch (JSONException e) { + e.printStackTrace(); + } + + syncEvents(); + } + + void syncEvents() { + + JSONArray trackEventList = getEventListFromLocalStorage(); + if (trackEventList.length() > 0) { + 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: { + String createdAt = ""; + if (event.has(IterableConstants.KEY_CREATED_AT)) { + createdAt = event.getString(IterableConstants.KEY_CREATED_AT); + } + if (event.has(IterableConstants.KEY_DATA_FIELDS)) { + JSONObject dataFields = new JSONObject(event.getString(IterableConstants.KEY_DATA_FIELDS)); + IterableApi.getInstance().track(event.getString(IterableConstants.KEY_EVENT_NAME), 0, 0, dataFields, createdAt); + } else { + IterableApi.getInstance().track(event.getString(IterableConstants.KEY_EVENT_NAME), 0, 0, null, createdAt); + } + break; + } + case IterableConstants.TRACK_PURCHASE: { + Gson gson = new GsonBuilder().create(); + Type listType = new TypeToken>() { + }.getType(); + List list = gson.fromJson(event.getString(IterableConstants.KEY_ITEMS), listType); + JSONObject userObject = new JSONObject(); + userObject.put(IterableConstants.KEY_CREATE_NEW_FIELDS, true); + + String createdAt = ""; + if (event.has(IterableConstants.KEY_CREATED_AT)) { + createdAt = event.getString(IterableConstants.KEY_CREATED_AT); + } + if (event.has(IterableConstants.KEY_DATA_FIELDS)) { + JSONObject dataFields = new JSONObject(event.getString(IterableConstants.KEY_DATA_FIELDS)); + IterableApi.getInstance().trackPurchase(event.getDouble(IterableConstants.KEY_TOTAL), list, dataFields, userObject, createdAt); + } else { + IterableApi.getInstance().trackPurchase(event.getDouble(IterableConstants.KEY_TOTAL), list, null, userObject, createdAt); + } + break; + } + case IterableConstants.TRACK_UPDATE_CART: { + Gson gson = new GsonBuilder().create(); + Type listType = new TypeToken>() { + }.getType(); + List list = gson.fromJson(event.getString(IterableConstants.KEY_ITEMS), listType); + JSONObject userObject = new JSONObject(); + userObject.put(IterableConstants.KEY_PREFER_USER_ID, true); + userObject.put(IterableConstants.KEY_MERGE_NESTED_OBJECTS, true); + userObject.put(IterableConstants.KEY_CREATE_NEW_FIELDS, true); + String createdAt = ""; + if (event.has(IterableConstants.KEY_CREATED_AT)) { + createdAt = event.getString(IterableConstants.KEY_CREATED_AT); + } + IterableApi.getInstance().updateCart(list, userObject, createdAt); + break; + } + default: + break; + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + clearLocallyStoredData(); + } + + private void clearLocallyStoredData() { + SharedPreferences sharedPref = IterableApi.sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(IterableConstants.SHARED_PREFS_ANON_SESSIONS, ""); + editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, ""); + editor.apply(); + } + + private void storeEventListToLocalStorage(JSONObject newDataObject) { + JSONArray previousDataArray = getEventListFromLocalStorage(); + previousDataArray.put(newDataObject); + SharedPreferences sharedPref = IterableApi.sharedInstance.getMainActivityContext().getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, previousDataArray.toString()); + editor.apply(); + + if (checkCriteriaCompletion()) { + createKnownUser(); + } + } + + private JSONArray getEventListFromLocalStorage() { + SharedPreferences sharedPref = IterableApi.sharedInstance.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 long getCurrentTime() { + return Calendar.getInstance().getTimeInMillis(); + } + + private String getCurrentDateTime() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.format(new Date()); + } +} \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java new file mode 100644 index 000000000..65db20e89 --- /dev/null +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java @@ -0,0 +1,56 @@ +package com.iterable.iterableapi; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +public class AnonymousUserMerge { + private static final AnonymousUserManager anonymousUserManager = new AnonymousUserManager(); + void mergeUserUsingUserId(IterableApiClient apiClient, String destinationUserId) { + String sourceUserId = IterableApi.getInstance().getUserId(); + if (sourceUserId == null || sourceUserId.isEmpty()) { + return; + } + apiClient.getUserByUserID(sourceUserId, new IterableHelper.IterableActionHandler() { + @Override + public void execute(@Nullable String data) { + if(data != null) { + try { + JSONObject dataObj = new JSONObject(data); + if (dataObj.has("user")){ + callMergeApi(apiClient, "", sourceUserId, IterableApi.getInstance().getEmail(), destinationUserId); + } + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + } + }); + } + + void mergeUserUsingEmail(IterableApiClient apiClient, String destinationEmail) { + String sourceEmail = IterableApi.getInstance().getUserId(); + if (sourceEmail == null || sourceEmail.isEmpty()) { + return; + } + apiClient.getUserByEmail(sourceEmail, new IterableHelper.IterableActionHandler() { + @Override + public void execute(@Nullable String data) { + if(data != null) { + callMergeApi(apiClient, destinationEmail, "", destinationEmail, IterableApi.getInstance().getUserId()); + } + } + }); + } + + private void callMergeApi(IterableApiClient apiClient, String sourceEmail, String sourceUserId, String destinationEmail, String destinationUserId) { + apiClient.mergeUser(sourceEmail, sourceUserId, destinationEmail, destinationUserId, new IterableHelper.SuccessHandler() { + @Override + public void onSuccess(@NonNull JSONObject data) { + anonymousUserManager.syncEvents(); + } + }, null); + } +} \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index b23c3d1fa..63c0e557b 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -44,6 +44,8 @@ public class IterableApi { private IterableHelper.FailureHandler _setUserFailureCallbackHandler; IterableApiClient apiClient = new IterableApiClient(new IterableApiAuthProvider()); + private static final AnonymousUserManager anonymousUserManager = new AnonymousUserManager(); + private static final AnonymousUserMerge anonymousUserMerge = new AnonymousUserMerge(); private @Nullable IterableInAppManager inAppManager; private String inboxSessionId; private IterableAuthManager authManager; @@ -536,9 +538,12 @@ public static void initialize(@NonNull Context context, @NonNull String apiKey, dataFields.put(IterableConstants.KEY_FIRETV, deviceDetails); sharedInstance.apiClient.updateUser(dataFields, false); } catch (JSONException e) { - IterableLogger.e(TAG, "initialize: exception", e); + IterableLogger.e(TAG, "initialize: exception", e); } } + + anonymousUserManager.updateAnonSession(); + anonymousUserManager.getCriteria(); } public static void setContext(Context context) { @@ -603,6 +608,7 @@ public void setEmail(@Nullable String email, @Nullable String authToken) { } public void setEmail(@Nullable String email, @Nullable String authToken, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) { + anonymousUserMerge.mergeUserUsingEmail(apiClient, email); //Only if passed in same non-null email if (_email != null && _email.equals(email)) { checkAndUpdateAuthToken(authToken); @@ -637,6 +643,7 @@ public void setUserId(@Nullable String userId, @Nullable String authToken) { } public void setUserId(@Nullable String userId, @Nullable String authToken, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) { + anonymousUserMerge.mergeUserUsingUserId(apiClient, userId); //If same non null userId is passed if (_userId != null && _userId.equals(userId)) { checkAndUpdateAuthToken(authToken); @@ -892,24 +899,48 @@ 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()) { + anonymousUserManager.trackAnonEvent(eventName, dataFields); return; } apiClient.track(eventName, campaignId, templateId, dataFields); } + /** + * Track an event. + * + * @param eventName + * @param dataFields + */ + public void track(@NonNull String eventName, int campaignId, int templateId, @Nullable JSONObject dataFields, String createdAt) { + IterableLogger.printInfo(); + apiClient.track(eventName, campaignId, templateId, dataFields, createdAt); + } + /** * Updates the status of the cart * @param items */ public void updateCart(@NonNull List items) { if (!checkSDKInitialization()) { + anonymousUserManager.trackAnonUpdateCart(items); return; } apiClient.updateCart(items); } + /** + * Updates the status of the cart + * + * @param items + * @param userObject + * @param createdAt + */ + public void updateCart(@NonNull List items, JSONObject userObject, String createdAt) { + apiClient.updateCart(items, userObject, createdAt); + } + /** * Tracks a purchase. * @param total total purchase amount @@ -927,12 +958,26 @@ public void trackPurchase(double total, @NonNull List items) { */ public void trackPurchase(double total, @NonNull List items, @Nullable JSONObject dataFields) { if (!checkSDKInitialization()) { + anonymousUserManager.trackAnonPurchaseEvent(total, items, dataFields); return; } apiClient.trackPurchase(total, items, dataFields); } + /** + * Tracks a purchase. + * + * @param total total purchase amount + * @param items list of purchased items + * @param dataFields a `JSONObject` containing any additional information to save along with the event + * @param userObject a `JSONObject` containing user data + * + */ + public void trackPurchase(double total, @NonNull List items, @Nullable JSONObject dataFields, JSONObject userObject, String createdAt) { + apiClient.trackPurchase(total, items, dataFields, userObject, createdAt); + } + /** * Updates the current user's email. * Also updates the current email in this IterableAPI instance if the API call was successful. diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java index e190c0aa0..7e512401b 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java @@ -97,6 +97,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 +143,26 @@ public void updateCart(@NonNull List items) { } } + public void updateCart(@NonNull List items, JSONObject userObject, String createdAt) { + JSONObject requestJSON = new JSONObject(); + + try { + JSONArray itemsArray = new JSONArray(); + for (CommerceItem item : items) { + itemsArray.put(item.toJSONObject()); + } + + addEmailOrUserIdToJson(userObject); + 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) { JSONObject requestJSON = new JSONObject(); try { @@ -142,6 +187,31 @@ public void trackPurchase(double total, @NonNull List items, @Null } } + public void trackPurchase(double total, @NonNull List items, @Nullable JSONObject dataFields, JSONObject userObject, String createdAt) { + JSONObject requestJSON = new JSONObject(); + try { + JSONArray itemsArray = new JSONArray(); + for (CommerceItem item : items) { + itemsArray.put(item.toJSONObject()); + } + + addEmailOrUserIdToJson(userObject); + 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(); @@ -553,4 +623,40 @@ void onLogout() { getRequestProcessor().onLogout(authProvider.getContext()); authProvider.resetAuth(); } + + void getUserByUserID(String userId, @Nullable IterableHelper.IterableActionHandler actionHandler) { + JSONObject requestJson = new JSONObject(); + try { + requestJson.put(IterableConstants.ANON_USER_ID, userId); + sendGetRequest(IterableConstants.ENDPOINT_GET_USER_BY_USERID, requestJson, actionHandler); + } + catch(JSONException e) { + e.printStackTrace(); + } + } + + void getUserByEmail(String email, @Nullable IterableHelper.IterableActionHandler actionHandler) { + JSONObject requestJson = new JSONObject(); + try { + requestJson.put(IterableConstants.ANON_USER_EMAIL, email); + sendGetRequest(IterableConstants.ENDPOINT_GET_USER_BY_EMAIL, requestJson, actionHandler); + } + catch(JSONException e) { + e.printStackTrace(); + } + } + + void mergeUser(String sourceEmail, String sourceUserId, String destinationEmail, String destinationUserId, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) { + JSONObject requestJson = new JSONObject(); + try { + requestJson.put(IterableConstants.SOURCE_EMAIL, sourceEmail); + requestJson.put(IterableConstants.SOURCE_USER_ID, sourceUserId); + requestJson.put(IterableConstants.DESTINATION_EMAIL, destinationEmail); + requestJson.put(IterableConstants.DESTINATION_USER_ID, destinationUserId); + sendPostRequest(IterableConstants.ENDPOINT_MERGE_USER, requestJson, successHandler, failureHandler); + } + catch(JSONException e) { + e.printStackTrace(); + } + } } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index 985d60480..58a1a4a1e 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -55,6 +55,7 @@ public final class IterableConstants { public static final String KEY_INBOX_SESSION_ID = "inboxSessionId"; public static final String KEY_OFFLINE_MODE = "offlineMode"; public static final String KEY_FIRETV = "FireTV"; + public static final String KEY_CREATE_NEW_FIELDS = "createNewFields"; //API Endpoint Key Constants public static final String ENDPOINT_DISABLE_DEVICE = "users/disableDevice"; @@ -75,6 +76,9 @@ public final class IterableConstants { public static final String ENDPOINT_UPDATE_USER_SUBS = "users/updateSubscriptions"; public static final String ENDPOINT_TRACK_INAPP_CLOSE = "events/trackInAppClose"; public static final String ENDPOINT_GET_REMOTE_CONFIGURATION = "mobile/getRemoteConfiguration"; + 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 PUSH_APP_ID = "IterableAppId"; public static final String PUSH_GCM_PROJECT_NUMBER = "GCMProjectNumber"; @@ -113,6 +117,16 @@ 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_ANON_SESSIONS = "itbl_anon_sessions"; + public static final String SHARED_PREFS_SESSION_NO = "number_of_sessions"; + public static final String SHARED_PREFS_LAST_SESSION = "last_session"; + public static final String SHARED_PREFS_FIRST_SESSION = "first_session"; + public static final String SHARED_PREFS_EVENT_TYPE = "dataType"; + public static final String SHARED_PREFS_CRITERIA = "criteria"; + public static final String SHARED_PREFS_CRITERIA_LIST = "criteriaList"; + public static final String SHARED_PREFS_CRITERIA_TYPE = "dataType"; + public static final String SHARED_PREFS_AGGREGATE_COUNT = "aggregateCount"; //Action buttons public static final String ITBL_BUTTON_IDENTIFIER = "identifier"; @@ -252,4 +266,17 @@ public final class IterableConstants { public static final String NO_MESSAGES_TITLE = "noMessagesTitle"; public static final String NO_MESSAGES_BODY = "noMessagesBody"; + + //Tracking types + public static final String TRACK_EVENT = "track"; + public static final String TRACK_PURCHASE = "purchase"; + public static final String TRACK_UPDATE_CART = "cartUpdate"; + public static final String TRACK_TOKEN_REGISTRATION = "tokenRegistration"; + + public static final String ANON_USER_ID = "userId"; + public static final String ANON_USER_EMAIL = "email"; + 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"; } \ 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..f5e714fd3 --- /dev/null +++ b/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java @@ -0,0 +1,331 @@ +package com.iterable.iterableapi.util; + +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.Iterator; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class CriteriaCompletionChecker { + + private JSONArray localStoredEventList; + + public Integer getMatchedCriteria(String criteriaData, JSONArray localStoredEventList) { + this.localStoredEventList = localStoredEventList; + Integer criteriaId = null; + + try { + JSONObject json = new JSONObject(criteriaData); + if (json.has("criteriaList")) { + JSONArray criteriaList = json.getJSONArray("criteriaList"); + for (int i = 0; i < criteriaList.length(); i++) { + JSONObject criteria = criteriaList.getJSONObject(i); + if (criteria.has("searchQuery") && criteria.has("criteriaId")) { + + JSONObject searchQuery = criteria.getJSONObject("searchQuery"); + int currentCriteriaId = criteria.getInt("criteriaId"); + JSONArray eventsToProcess = getEventsWithCartItems(); + JSONArray nonPurchaseEvents = getNonCartEvents(); + for (int j = 0; j < nonPurchaseEvents.length(); j++) { + eventsToProcess.put(nonPurchaseEvents.getJSONObject(j)); + } + + boolean result = evaluateTree(searchQuery, eventsToProcess); + if (result) { + criteriaId = currentCriteriaId; + break; + } + } + } + } + } catch (JSONException e) { + handleJSONException(e); + } + return criteriaId; + } + + 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) + || (localEventData.get(IterableConstants.SHARED_PREFS_EVENT_TYPE).equals(IterableConstants.TRACK_UPDATE_CART)))) { + + JSONObject updatedItem = new JSONObject(); + if (localEventData.has(IterableConstants.KEY_ITEMS)) { + + JSONArray items = new JSONArray(localEventData.getString(IterableConstants.KEY_ITEMS)); + for (int j = 0; j < items.length(); j++) { + JSONObject item = items.getJSONObject(j); + Iterator itemKeys = item.keys(); + + while (itemKeys.hasNext()) { + String key = itemKeys.next(); + updatedItem.put("shoppingCartItems." + key, item.get(key)); + } + } + } + + if (localEventData.has("dataFields")) { + JSONObject dataFields = localEventData.getJSONObject("dataFields"); + 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("dataFields")) { + 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)) { + nonPurchaseEvents.put(localEventData); + } + } + } catch (JSONException e) { + handleJSONException(e); + } + return nonPurchaseEvents; + } + + public boolean evaluateTree(JSONObject node, JSONArray localEventData) { + try { + if (node.has("searchQueries")) { + String combinator = node.getString("combinator"); + JSONArray searchQueries = node.getJSONArray("searchQueries"); + 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 (node.has("searchCombo")) { + JSONObject searchCombo = node.getJSONObject("searchCombo"); + return evaluateTree(searchCombo, localEventData); + } else if (node.has("field")) { + return evaluateField(node, localEventData); + } + } catch (Exception e) { + handleException(e); + } + return false; + } + + private boolean evaluateField(JSONObject node, JSONArray localEventData) { + try { + return evaluateFieldLogic(node, localEventData); + } catch (JSONException e) { + handleJSONException(e); + } + return false; + } + + private boolean evaluateFieldLogic(JSONObject node, 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("dataType"); + if (!dataType.equals(trackingType)) { + return false; + } + + String field = node.getString("field"); + String comparatorType = node.getString("comparatorType"); + ArrayList localDataKeys = extractKeys(eventData); + + for (String key : localDataKeys) { + if (field.equals(key)) { + Object matchedCountObj = eventData.get(key); + if (evaluateComparison(comparatorType, matchedCountObj, node.getString("value"))) { + return true; + } + } + } + } + + return false; + } + + private boolean evaluateComparison(String comparatorType, Object matchObj, String valueToCompare) { + if (valueToCompare == null) { + return false; + } + + switch (comparatorType) { + case "Equals": + return compareValueEquality(matchObj, valueToCompare); + case "DoesNotEquals": + return !compareValueEquality(matchObj, valueToCompare); + case "GreaterThan": + return compareNumericValues(matchObj, valueToCompare, " > "); + case "LessThan": + return compareNumericValues(matchObj, valueToCompare, " < "); + case "GreaterThanOrEqualTo": + return compareNumericValues(matchObj, valueToCompare, " >= "); + case "LessThanOrEqualTo": + return compareNumericValues(matchObj, valueToCompare, " <= "); + case "Contains": + return compareStringContains(matchObj, valueToCompare); + case "StartsWith": + return compareStringStartsWith(matchObj, valueToCompare); + case "MatchesRegex": + return compareWithRegex(matchObj instanceof String ? (String) matchObj : "", valueToCompare); + default: + return false; + } + } + + private boolean compareValueEquality(Object sourceTo, String stringValue) { + if (sourceTo instanceof Double && isDouble(stringValue)) { + return ((Double) sourceTo).equals(Double.parseDouble(stringValue)); + } else if (sourceTo instanceof Integer && isInteger(stringValue)) { + return ((Integer) sourceTo).equals(Integer.parseInt(stringValue)); + } else if (sourceTo instanceof Long && isLong(stringValue)) { + return ((Long) sourceTo).equals(Long.parseLong(stringValue)); + } else if (sourceTo instanceof Boolean && isBoolean(stringValue)) { + return ((Boolean) sourceTo).equals(Boolean.parseBoolean(stringValue)); + } else if (sourceTo instanceof String) { + return ((String) sourceTo).equals(stringValue); + } + return false; + } + + private boolean compareNumericValues(Object sourceTo, String stringValue, String compareOperator) { + 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 compareStringContains(Object sourceTo, String stringValue) { + return sourceTo instanceof String && ((String) sourceTo).contains(stringValue); + } + + private boolean compareStringStartsWith(Object sourceTo, String stringValue) { + return sourceTo instanceof String && ((String) sourceTo).startsWith(stringValue); + } + + private boolean compareWithRegex(String sourceTo, String pattern) { + try { + Pattern regexPattern = Pattern.compile(pattern); + return regexPattern.matcher(sourceTo).matches(); + } catch (PatternSyntaxException e) { + e.printStackTrace(); + 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 ArrayList extractKeys(JSONObject jsonObject) { + ArrayList keys = new ArrayList<>(); + Iterator jsonKeys = jsonObject.keys(); + while (jsonKeys.hasNext()) { + keys.add(jsonKeys.next()); + } + return keys; + } + + 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 From e01f311f8217b62e644a7cd3e19468adc63b970a Mon Sep 17 00:00:00 2001 From: hani Date: Mon, 18 Dec 2023 18:26:18 +0530 Subject: [PATCH 002/248] Resolve build failed points --- .../com/iterable/iterableapi/AnonymousUserMerge.java | 6 +++--- .../java/com/iterable/iterableapi/IterableApiClient.java | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java index 65db20e89..13848655b 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java @@ -16,10 +16,10 @@ void mergeUserUsingUserId(IterableApiClient apiClient, String destinationUserId) apiClient.getUserByUserID(sourceUserId, new IterableHelper.IterableActionHandler() { @Override public void execute(@Nullable String data) { - if(data != null) { + if (data != null) { try { JSONObject dataObj = new JSONObject(data); - if (dataObj.has("user")){ + if (dataObj.has("user")) { callMergeApi(apiClient, "", sourceUserId, IterableApi.getInstance().getEmail(), destinationUserId); } } catch (JSONException e) { @@ -38,7 +38,7 @@ void mergeUserUsingEmail(IterableApiClient apiClient, String destinationEmail) { apiClient.getUserByEmail(sourceEmail, new IterableHelper.IterableActionHandler() { @Override public void execute(@Nullable String data) { - if(data != null) { + if (data != null) { callMergeApi(apiClient, destinationEmail, "", destinationEmail, IterableApi.getInstance().getUserId()); } } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java index 7e512401b..4cb78a30d 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java @@ -629,8 +629,7 @@ void getUserByUserID(String userId, @Nullable IterableHelper.IterableActionHandl try { requestJson.put(IterableConstants.ANON_USER_ID, userId); sendGetRequest(IterableConstants.ENDPOINT_GET_USER_BY_USERID, requestJson, actionHandler); - } - catch(JSONException e) { + } catch (JSONException e) { e.printStackTrace(); } } @@ -640,8 +639,7 @@ void getUserByEmail(String email, @Nullable IterableHelper.IterableActionHandler try { requestJson.put(IterableConstants.ANON_USER_EMAIL, email); sendGetRequest(IterableConstants.ENDPOINT_GET_USER_BY_EMAIL, requestJson, actionHandler); - } - catch(JSONException e) { + } catch (JSONException e) { e.printStackTrace(); } } @@ -654,8 +652,7 @@ void mergeUser(String sourceEmail, String sourceUserId, String destinationEmail, requestJson.put(IterableConstants.DESTINATION_EMAIL, destinationEmail); requestJson.put(IterableConstants.DESTINATION_USER_ID, destinationUserId); sendPostRequest(IterableConstants.ENDPOINT_MERGE_USER, requestJson, successHandler, failureHandler); - } - catch(JSONException e) { + } catch (JSONException e) { e.printStackTrace(); } } From 25ee59cef101d82ead5994393ae2d0a8a4b62333 Mon Sep 17 00:00:00 2001 From: hani Date: Thu, 21 Dec 2023 13:41:21 +0530 Subject: [PATCH 003/248] Add doc for anon user merge --- AnonymousUserMerge.md | 38 +++++++++++++++++++ .../iterableapi/AnonymousUserManager.java | 2 +- .../iterableapi/AnonymousUserMerge.java | 15 +++++--- .../iterableapi/IterableApiClient.java | 16 ++++++-- 4 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 AnonymousUserMerge.md diff --git a/AnonymousUserMerge.md b/AnonymousUserMerge.md new file mode 100644 index 000000000..9bb51b4fd --- /dev/null +++ b/AnonymousUserMerge.md @@ -0,0 +1,38 @@ +# AnonymousUserMerge Class + +## Class Introduction + +The `AnonymousUserMerge` class is responsible for merging anonymous user with logged-in one. +It includes methods for merge user by userId and emailId. +We call methods of this class internally to merge user when setUserId or setEmail method call. After merge we sync events through Iterable API. + +## Class Structure + +The `AnonymousUserMerge` class includes the following key components: + +- **Methods:** + - `mergeUserUsingUserId(apiClient: IterableApiClient, destinationUserId: String)`: Merge user using userID if anonymous user exists and sync events + - `mergeUserUsingEmail(apiClient: IterableApiClient, destinationEmail: String)`: Merge user using emailId if anonymous user exists and sync events + - `callMergeApi(apiClient: IterableApiClient, sourceEmail: String, sourceUserId: String, destinationEmail: String, destinationUserId: String)`: Call API to merge user and sync remaining events. + +## Methods Description + +### `mergeUserUsingUserId(apiClient: IterableApiClient, destinationUserId: String)` + +This method merge the anonymous user with the logged-in one. It does the following: + +* Check for user exists using userId. +* If user exists then call the merge user API. + +### `mergeUserUsingEmail(apiClient: IterableApiClient, destinationEmail: String)` + +This method merge the anonymous user with the logged-in one. It does the following: + +* Check for user exists using emailId. +* If user exists then call the merge user API. + +### `callMergeApi(apiClient: IterableApiClient, sourceEmail: String, sourceUserId: String, destinationEmail: String, destinationUserId: String)` + +This method call API to merge user. It does the following: + +* Call the Iterable API and sync remaining events. \ No newline at end of file diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java index 5df2a9882..93c67abd0 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java @@ -26,7 +26,7 @@ public class AnonymousUserManager { - private static final String TAG = "RNIterableAPIModule"; + private static final String TAG = "AnonymousUserManager"; void updateAnonSession() { IterableLogger.v(TAG, "updateAnonSession"); diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java index 13848655b..117dca53c 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserMerge.java @@ -8,12 +8,15 @@ public class AnonymousUserMerge { private static final AnonymousUserManager anonymousUserManager = new AnonymousUserManager(); + private static final String TAG = "AnonymousUserMerge"; + void mergeUserUsingUserId(IterableApiClient apiClient, String destinationUserId) { String sourceUserId = IterableApi.getInstance().getUserId(); - if (sourceUserId == null || sourceUserId.isEmpty()) { + if (sourceUserId == null || sourceUserId.isEmpty() || sourceUserId.equals(destinationUserId)) { + IterableLogger.d(TAG, "sourceUserId is null or same as destinationUserId"); return; } - apiClient.getUserByUserID(sourceUserId, new IterableHelper.IterableActionHandler() { + apiClient.getUserByUserID(destinationUserId, new IterableHelper.IterableActionHandler() { @Override public void execute(@Nullable String data) { if (data != null) { @@ -31,13 +34,15 @@ public void execute(@Nullable String data) { } void mergeUserUsingEmail(IterableApiClient apiClient, String destinationEmail) { - String sourceEmail = IterableApi.getInstance().getUserId(); - if (sourceEmail == null || sourceEmail.isEmpty()) { + String sourceEmail = IterableApi.getInstance().getEmail(); + if (sourceEmail == null || sourceEmail.isEmpty() || sourceEmail.equals(destinationEmail)) { + IterableLogger.d(TAG, "sourceEmail is null or same as destinationEmail"); return; } - apiClient.getUserByEmail(sourceEmail, new IterableHelper.IterableActionHandler() { + apiClient.getUserByEmail(destinationEmail, new IterableHelper.IterableActionHandler() { @Override public void execute(@Nullable String data) { + IterableLogger.d(TAG, "data of email: " + data); if (data != null) { callMergeApi(apiClient, destinationEmail, "", destinationEmail, IterableApi.getInstance().getUserId()); } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java index 4cb78a30d..57b9035b8 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java @@ -647,10 +647,18 @@ void getUserByEmail(String email, @Nullable IterableHelper.IterableActionHandler void mergeUser(String sourceEmail, String sourceUserId, String destinationEmail, String destinationUserId, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) { JSONObject requestJson = new JSONObject(); try { - requestJson.put(IterableConstants.SOURCE_EMAIL, sourceEmail); - requestJson.put(IterableConstants.SOURCE_USER_ID, sourceUserId); - requestJson.put(IterableConstants.DESTINATION_EMAIL, destinationEmail); - requestJson.put(IterableConstants.DESTINATION_USER_ID, destinationUserId); + 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(); From 57e0803198cf7c994aa5249bc74ab333d540a358 Mon Sep 17 00:00:00 2001 From: hani Date: Wed, 10 Jan 2024 14:55:41 +0530 Subject: [PATCH 004/248] sync event if user is not created --- .../main/java/com/iterable/iterableapi/IterableApi.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 63c0e557b..5d8387dab 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -657,7 +657,12 @@ public void setUserId(@Nullable String userId, @Nullable String authToken, @Null logoutPreviousUser(); _email = null; - _userId = userId; + if (_userId == null) { + _userId = userId; + anonymousUserManager.syncEvents(); + } else { + _userId = userId; + } _setUserSuccessCallbackHandler = successHandler; _setUserFailureCallbackHandler = failureHandler; storeAuthData(); From 6cab48e36a5fec6e82e33902c31e0819a366f5a9 Mon Sep 17 00:00:00 2001 From: hani Date: Wed, 10 Jan 2024 16:40:21 +0530 Subject: [PATCH 005/248] implement API call for get criteria list --- .../iterableapi/AnonymousUserManager.java | 124 ++---------------- .../iterableapi/IterableApiClient.java | 5 + .../iterableapi/IterableConstants.java | 3 +- .../util/CriteriaCompletionChecker.java | 4 +- 4 files changed, 22 insertions(+), 114 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java index 93c67abd0..2740615aa 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/AnonymousUserManager.java @@ -119,117 +119,19 @@ void trackAnonUpdateCart(@NonNull List items) { } void getCriteria() { - try { - // call API when it is available and save data in SharedPreferences, until then just save the data using static data - String mockData = "{" + - " \"count\":2," + - " \"criteriaList\":[" + - " {" + - " \"criteriaId\":12345," + - " \"searchQuery\":{" + - " \"combinator\":\"Or\"," + - " \"searchQueries\":[" + - " {" + - " \"combinator\":\"And\"," + - " \"searchQueries\":[" + - " {" + - " \"dataType\":\"purchase\"," + - " \"searchCombo\":{" + - " \"combinator\":\"And\"," + - " \"searchQueries\":[" + - " {" + - " \"field\":\"shoppingCartItems.price\"," + - " \"fieldType\":\"double\"," + - " \"comparatorType\":\"Equals\"," + - " \"dataType\":\"purchase\"," + - " \"id\":2," + - " \"value\":\"4.67\"" + - " }," + - " {" + - " \"field\":\"shoppingCartItems.quantity\"," + - " \"fieldType\":\"long\"," + - " \"comparatorType\":\"GreaterThanOrEqualTo\"," + - " \"dataType\":\"purchase\"," + - " \"id\":3," + - " \"valueLong\":2," + - " \"value\":\"2\"" + - " }" + - " ]" + - " }" + - " }" + - " ]" + - " }" + - " ]" + - " }" + - " }," + - " {" + - " \"criteriaId\":5678," + - " \"searchQuery\":{" + - " \"combinator\":\"Or\"," + - " \"searchQueries\":[" + - " {" + - " \"combinator\":\"Or\"," + - " \"searchQueries\":[" + - " {" + - " \"dataType\":\"user\"," + - " \"searchCombo\":{" + - " \"combinator\":\"And\"," + - " \"searchQueries\":[" + - " {" + - " \"field\":\"itblInternal.emailDomain\"," + - " \"fieldType\":\"string\"," + - " \"comparatorType\":\"Equals\"," + - " \"dataType\":\"user\"," + - " \"id\":6," + - " \"value\":\"gmail.com\"" + - " }" + - " ]" + - " }" + - " }," + - " {" + - " \"dataType\":\"customEvent\"," + - " \"searchCombo\":{" + - " \"combinator\":\"And\"," + - " \"searchQueries\":[" + - " {" + - " \"field\":\"eventName\"," + - " \"fieldType\":\"string\"," + - " \"comparatorType\":\"Equals\"," + - " \"dataType\":\"customEvent\"," + - " \"id\":9," + - " \"value\":\"processing_cancelled\"" + - " }," + - " {" + - " \"field\":\"createdAt\"," + - " \"fieldType\":\"date\"," + - " \"comparatorType\":\"GreaterThan\"," + - " \"dataType\":\"customEvent\"," + - " \"id\":10," + - " \"dateRange\":{" + - " }," + - " \"isRelativeDate\":false," + - " \"value\":\"1688194800000\"" + - " }" + - " ]" + - " }" + - " }" + - " ]" + - " }" + - " ]" + - " }" + - " }" + - " ]" + - "}"; - - JSONObject mockDataObject = new JSONObject(mockData); - SharedPreferences sharedPref = IterableApi.sharedInstance.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(); - } + IterableApi.getInstance().apiClient.getCriteriaList(data -> { + if (data != null) { + try { + JSONObject mockDataObject = new JSONObject(data); + SharedPreferences sharedPref = IterableApi.sharedInstance.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 boolean checkCriteriaCompletion() { diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java index 57b9035b8..9e5617a90 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApiClient.java @@ -664,4 +664,9 @@ void mergeUser(String sourceEmail, String sourceUserId, String destinationEmail, e.printStackTrace(); } } + + void getCriteriaList(@Nullable IterableHelper.IterableActionHandler actionHandler) { + sendGetRequest(IterableConstants.ENDPOINT_CRITERIA_LIST, new JSONObject(), actionHandler); + } + } diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java index 58a1a4a1e..c68686f2d 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableConstants.java @@ -79,6 +79,7 @@ public final class IterableConstants { 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_CRITERIA_LIST = "anonymoususer/list"; public static final String PUSH_APP_ID = "IterableAppId"; public static final String PUSH_GCM_PROJECT_NUMBER = "GCMProjectNumber"; @@ -268,7 +269,7 @@ public final class IterableConstants { public static final String NO_MESSAGES_BODY = "noMessagesBody"; //Tracking types - public static final String TRACK_EVENT = "track"; + 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 TRACK_TOKEN_REGISTRATION = "tokenRegistration"; diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java b/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java index f5e714fd3..af73b8948 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java @@ -22,8 +22,8 @@ public Integer getMatchedCriteria(String criteriaData, JSONArray localStoredEven try { JSONObject json = new JSONObject(criteriaData); - if (json.has("criteriaList")) { - JSONArray criteriaList = json.getJSONArray("criteriaList"); + if (json.has("criterias")) { + JSONArray criteriaList = json.getJSONArray("criterias"); for (int i = 0; i < criteriaList.length(); i++) { JSONObject criteria = criteriaList.getJSONObject(i); if (criteria.has("searchQuery") && criteria.has("criteriaId")) { From 6d9ced1b8ccac2887491c9da6da1c2d7bfc98d99 Mon Sep 17 00:00:00 2001 From: hani Date: Thu, 11 Jan 2024 11:24:11 +0530 Subject: [PATCH 006/248] codeclimate related changes --- .../util/CriteriaCompletionChecker.java | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java b/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java index af73b8948..cc28a6a78 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/util/CriteriaCompletionChecker.java @@ -24,32 +24,55 @@ public Integer getMatchedCriteria(String criteriaData, JSONArray localStoredEven JSONObject json = new JSONObject(criteriaData); if (json.has("criterias")) { JSONArray criteriaList = json.getJSONArray("criterias"); - for (int i = 0; i < criteriaList.length(); i++) { - JSONObject criteria = criteriaList.getJSONObject(i); - if (criteria.has("searchQuery") && criteria.has("criteriaId")) { - - JSONObject searchQuery = criteria.getJSONObject("searchQuery"); - int currentCriteriaId = criteria.getInt("criteriaId"); - JSONArray eventsToProcess = getEventsWithCartItems(); - JSONArray nonPurchaseEvents = getNonCartEvents(); - for (int j = 0; j < nonPurchaseEvents.length(); j++) { - eventsToProcess.put(nonPurchaseEvents.getJSONObject(j)); - } + criteriaId = findMatchedCriteria(criteriaList); + } + } catch (JSONException e) { + handleJSONException(e); + } - boolean result = evaluateTree(searchQuery, eventsToProcess); - if (result) { - criteriaId = currentCriteriaId; - break; - } + return criteriaId; + } + + private Integer findMatchedCriteria(JSONArray criteriaList) { + Integer criteriaId = null; + + for (int i = 0; i < criteriaList.length(); i++) { + try { + JSONObject criteria = criteriaList.getJSONObject(i); + if (criteria.has("searchQuery") && criteria.has("criteriaId")) { + JSONObject searchQuery = criteria.getJSONObject("searchQuery"); + int currentCriteriaId = criteria.getInt("criteriaId"); + JSONArray eventsToProcess = prepareEventsToProcess(); + + boolean result = evaluateTree(searchQuery, eventsToProcess); + if (result) { + criteriaId = currentCriteriaId; + break; } } + } catch (JSONException e) { + throw new RuntimeException(e); } - } catch (JSONException e) { - handleJSONException(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 { From b30d583c24d39d0d1278004239e09a6173b3c967 Mon Sep 17 00:00:00 2001 From: hani Date: Wed, 17 Jan 2024 10:46:51 +0530 Subject: [PATCH 007/248] resolve error --- .../main/java/com/iterable/iterableapi/IterableApi.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java index 63c0e557b..0c0aefe73 100644 --- a/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java +++ b/iterableapi/src/main/java/com/iterable/iterableapi/IterableApi.java @@ -608,7 +608,9 @@ public void setEmail(@Nullable String email, @Nullable String authToken) { } public void setEmail(@Nullable String email, @Nullable String authToken, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) { - anonymousUserMerge.mergeUserUsingEmail(apiClient, email); + if (email != null && !email.isEmpty()) { + anonymousUserMerge.mergeUserUsingEmail(apiClient, email); + } //Only if passed in same non-null email if (_email != null && _email.equals(email)) { checkAndUpdateAuthToken(authToken); @@ -643,7 +645,9 @@ public void setUserId(@Nullable String userId, @Nullable String authToken) { } public void setUserId(@Nullable String userId, @Nullable String authToken, @Nullable IterableHelper.SuccessHandler successHandler, @Nullable IterableHelper.FailureHandler failureHandler) { - anonymousUserMerge.mergeUserUsingUserId(apiClient, userId); + if (userId != null && !userId.isEmpty()) { + anonymousUserMerge.mergeUserUsingUserId(apiClient, userId); + } //If same non null userId is passed if (_userId != null && _userId.equals(userId)) { checkAndUpdateAuthToken(authToken); From c86fb581b0f32eb0c19db6f0597b8b17c854c40c Mon Sep 17 00:00:00 2001 From: hani Date: Wed, 17 Jan 2024 10:47:31 +0530 Subject: [PATCH 008/248] update sample app for demo --- .../com/iterable/androidsdk/MainActivity.java | 47 +++++++++++++++++ app/src/main/res/layout/content_main.xml | 50 +++++++++++++++++-- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/iterable/androidsdk/MainActivity.java b/app/src/main/java/com/iterable/androidsdk/MainActivity.java index c5f0725b8..f4bb1ca36 100644 --- a/app/src/main/java/com/iterable/androidsdk/MainActivity.java +++ b/app/src/main/java/com/iterable/androidsdk/MainActivity.java @@ -1,16 +1,27 @@ package com.iterable.androidsdk; 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 @@ -20,6 +31,8 @@ protected void onCreate(Bundle savedInstanceState) { Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); + IterableApi.initialize(this, "17886a58ef2148b0a7b4f3ea9d958e7f"); + FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override @@ -28,6 +41,40 @@ public void onClick(View view) { .setAction("Action", null).show(); } }); + + 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/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index 5646874e1..e4e51ed3f 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" /> + +