diff --git a/CHANGELOG.md b/CHANGELOG.md index 23bf8ada1..373180cb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,16 @@ This project adheres to [Semantic Versioning](http://semver.org/). - 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 Anonymous user activation, a feature that allows marketers to convert valuable visitors into customers. With this feature, the SDK can: + - Fetch anonymous profile creation criteria from your Iterable project, and then automatically create Iterable user profiles for anonymous users who meet these criteria. + - Save information about a user's previous interactions with your application to their anonymous profile, after it's created. + - Display personalized messages for anonymous users (in-app, push, and embedded messages). + - Merge anonymous profiles into an existing, known user profiles (when needed). +- Anonymous 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 + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 899da28b3..a1299cd4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,6 +53,7 @@ dependencies { implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.fragment:fragment:1.2.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' debugImplementation 'androidx.fragment:fragment-testing:1.2.4' implementation project(':iterableapi') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a3609abf3..b3bc47ec8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,13 +1,18 @@ - + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/iterable/androidsdk/AnonTrackingTestActivity.java b/app/src/main/java/com/iterable/androidsdk/AnonTrackingTestActivity.java new file mode 100644 index 000000000..786b053bc --- /dev/null +++ b/app/src/main/java/com/iterable/androidsdk/AnonTrackingTestActivity.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.IterableAnonUserHandler; +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 AnonTrackingTestActivity extends AppCompatActivity implements IterableAnonUserHandler, IterableAuthHandler { + + private CheckBox anonymousUsageTrackedCheckBox; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_test); + anonymousUsageTrackedCheckBox = findViewById(R.id.anonymousUsageTracked_check_box); + IterableConfig iterableConfig = new IterableConfig.Builder().setEnableAnonActivation(true).setIterableAnonUserHandler(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_ANON_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(anonymousUsageTrackedCheckBox.isChecked()); + + }, 1000); + + anonymousUsageTrackedCheckBox.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 onAnonUserCreated(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/java/com/iterable/androidsdk/MainActivity.java b/app/src/main/java/com/iterable/androidsdk/MainActivity.java index c5f0725b8..a788a14a5 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, AnonTrackingTestActivity.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/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"> + + + + + + + + + +