diff --git a/CHANGELOG.md b/CHANGELOG.md index 19e6d2662..cd5124329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,19 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Migrated embedded message OOTB views to use Material Design buttons for better UI consistency - Updated sample app Gradle configuration to use newer versions for better compatibility +## [3.6.0-beta3] + +### Added +- Added consent logging functionality for unknown user activation feature + +### Changed +- Enhanced unknown user activation with improved criteria fetching and user ID generation logic + +### Fixed +- Fixed unknown user activation to ensure criteria is fetched on foregrounding the app by default +- Fixed unknown user ID generation to only occur once when multiple track calls are made +- Fixed consent timestamp handling when consent is revoked + ## [3.5.14] ### Fixed - Fixed auth token refresh when app is in background, ensuring reliable token refresh in all app states. @@ -63,6 +76,14 @@ IterableApi.initialize(context, apiKey, config); - Added support for providing a list of placement ids to sync only certain placement ids. - support for pre-release automation +## [3.6.0-beta2] + +### Fixed +- This release includes fixes for the Unknown user activation private beta: + - Criteria is now fetched on foregrounding the app by default. This feature can be turned off setting enableForegroundCriteriaFetch flag to false. + - Unknown user ids are only generated once when multiple track calls are made. +- Unknown user activation is currently in private beta. If you'd like to learn more about it or discuss using it, talk to your Iterable customer success manager (who can also provide detailed documentation). + ## [3.5.10] ### Added @@ -109,6 +130,16 @@ IterableApi.initialize(context, apiKey, config); - Addressed a text truncation issue in Embedded Message templates for applications targeting Android 14 and Android 15. - Improved InboxActivity compatibility with edge-to-edge layouts, ensuring seamless handling of notches and display cutouts. +## [3.6.0-beta1] + +#### Added +- This release includes initial support for Unknown user activation, a feature that allows marketers to convert valuable visitors into customers. With this feature, the SDK can: + - Fetch unknown user profile creation criteria from your Iterable project, and then automatically create Iterable user profiles for unknown users who meet these criteria. + - Save information about a visitor's previous interactions with your application to their unknown user profile, after it's created. + - Display personalized messages for unknown users (in-app, push, and embedded messages). + - Merge unknown user profiles into an existing, known user profiles (when needed). +- Unknown user activation is currently in private beta. If you'd like to learn more about it or discuss using it, talk to your Iterable customer success manager (who can also provide detailed documentation). + ## [3.5.3] #### Fixed - Fixed an [issue](https://github.com/Iterable/react-native-sdk/issues/547) where the SDK would crash if the `IterableInAppMessage` object was null when consuming an in-app message. diff --git a/app/.idea/workspace.xml b/app/.idea/workspace.xml new file mode 100644 index 000000000..9c6424696 --- /dev/null +++ b/app/.idea/workspace.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1728484381438 + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b44c875ad..4348c851c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,6 +50,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.fragment:fragment:1.8.5' androidTestImplementation 'androidx.fragment:fragment-testing:1.8.5' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation project(':iterableapi') implementation project(':iterableapi-ui') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3b9b69d20..c77d25dba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,9 @@ android:supportsRtl="true" android:usesCleartextTraffic="true" android:theme="@style/AppTheme"> + + \ No newline at end of file diff --git a/app/src/main/java/com/iterable/androidsdk/MainActivity.java b/app/src/main/java/com/iterable/androidsdk/MainActivity.java index c5f0725b8..8e4ef2819 100644 --- a/app/src/main/java/com/iterable/androidsdk/MainActivity.java +++ b/app/src/main/java/com/iterable/androidsdk/MainActivity.java @@ -1,16 +1,28 @@ package com.iterable.androidsdk; +import android.content.Intent; import android.os.Bundle; + import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; + import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; + import android.view.View; import android.view.Menu; import android.view.MenuItem; +import com.iterable.iterableapi.CommerceItem; +import com.iterable.iterableapi.IterableApi; import com.iterable.iterableapi.testapp.R; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + public class MainActivity extends AppCompatActivity { @Override @@ -19,6 +31,8 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); + //Below api key is used to display merge user feature + IterableApi.initialize(this, "289895aa038648ee9e4ce60bd0a46e9c"); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @@ -28,6 +42,46 @@ public void onClick(View view) { .setAction("Action", null).show(); } }); + + findViewById(R.id.mainLayout).setOnLongClickListener(v -> { + Intent intent = new Intent(this, UnknownUserTrackingTestActivity.class); + startActivity(intent); + return true; + }); + + findViewById(R.id.btn_track_event).setOnClickListener(v -> IterableApi.getInstance().track("Browse Mocha")); + + findViewById(R.id.btn_update_cart).setOnClickListener(v -> { + List items = new ArrayList<>(); + items.add(new CommerceItem("123", "Mocha", 1, 1)); + IterableApi.getInstance().updateCart(items); + }); + + findViewById(R.id.btn_buy_mocha).setOnClickListener(v -> { + List items = new ArrayList<>(); + items.add(new CommerceItem("456", "Black Coffee", 2, 1)); + IterableApi.getInstance().trackPurchase(4, items); + }); + + findViewById(R.id.btn_buy_coffee).setOnClickListener(v -> { + List items = new ArrayList<>(); + items.add(new CommerceItem("456", "Black Coffee", 5, 1)); + IterableApi.getInstance().trackPurchase(5, items); + }); + + findViewById(R.id.btn_set_user).setOnClickListener(v -> IterableApi.getInstance().setUserId("hani7")); + + findViewById(R.id.btn_update_user).setOnClickListener(v -> { + try { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("firstName", "Hani"); + IterableApi.getInstance().updateUser(jsonObject); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + + findViewById(R.id.btn_logout).setOnClickListener(view -> IterableApi.getInstance().setUserId(null)); } @Override diff --git a/app/src/main/java/com/iterable/androidsdk/UnknownUserTrackingTestActivity.java b/app/src/main/java/com/iterable/androidsdk/UnknownUserTrackingTestActivity.java new file mode 100644 index 000000000..fcaaa0e71 --- /dev/null +++ b/app/src/main/java/com/iterable/androidsdk/UnknownUserTrackingTestActivity.java @@ -0,0 +1,205 @@ +package com.iterable.androidsdk; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.widget.CheckBox; +import android.widget.EditText; + +import com.iterable.iterableapi.AuthFailure; +import com.iterable.iterableapi.CommerceItem; +import com.iterable.iterableapi.IterableUnknownUserHandler; +import com.iterable.iterableapi.IterableApi; +import com.iterable.iterableapi.IterableAuthHandler; +import com.iterable.iterableapi.IterableConfig; +import com.iterable.iterableapi.IterableConstants; +import com.iterable.iterableapi.testapp.R; +import com.iterable.iterableapi.util.IterableJwtGenerator; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class UnknownUserTrackingTestActivity extends AppCompatActivity implements IterableUnknownUserHandler, IterableAuthHandler { + + private CheckBox unknownUsageTrackedCheckBox; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_test); + unknownUsageTrackedCheckBox = findViewById(R.id.unknownUsageTracked_check_box); + IterableConfig iterableConfig = new IterableConfig.Builder().setEnableUnknownUserActivation(true).setUnknownUserHandler(this).setAuthHandler(this).build(); + + // clear data for testing + SharedPreferences sharedPref = getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putString(IterableConstants.SHARED_PREFS_UNKNOWN_SESSIONS, ""); + editor.putString(IterableConstants.SHARED_PREFS_EVENT_LIST_KEY, ""); + editor.putBoolean(IterableConstants.SHARED_PREFS_VISITOR_USAGE_TRACKED, false); + editor.apply(); + + new Handler().postDelayed(() -> { + IterableApi.initialize(getBaseContext(), "bef41e4c1ab24b21bb3d65e47aa57a89", iterableConfig); + IterableApi.getInstance().setUserId(null); + IterableApi.getInstance().setEmail(null); + printAllSharedPreferencesData(this); + IterableApi.getInstance().setVisitorUsageTracked(unknownUsageTrackedCheckBox.isChecked()); + + }, 1000); + + unknownUsageTrackedCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + IterableApi.getInstance().setVisitorUsageTracked(isChecked); + }); + + findViewById(R.id.updateCart).setOnClickListener(view -> { + EditText updateCart_edit = findViewById(R.id.updateCart_edit); + if(updateCart_edit == null) return; + Log.d("TEST_USER", String.valueOf(updateCart_edit.getText())); + try { + JSONArray cartJSOnItems = new JSONArray(String.valueOf(updateCart_edit.getText())); + List items = new ArrayList<>(); + for(int i = 0; i < cartJSOnItems.length(); i++) { + final JSONObject item = cartJSOnItems.getJSONObject(i); + items.add(new CommerceItem(item.getString("id"), item.getString("name"), item.getDouble("price"), item.getInt("quantity"))); + } + IterableApi.getInstance().updateCart(items); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + findViewById(R.id.trackPurchase).setOnClickListener(view -> { + EditText purchase_items = findViewById(R.id.trackPurchase_edit); + if(purchase_items == null) return; + Log.d("TEST_USER", String.valueOf(purchase_items.getText())); + + int total; + + try { + JSONObject jsonData = new JSONObject(String.valueOf(purchase_items.getText())); + total = (int) jsonData.get("total"); + JSONArray items_array = jsonData.getJSONArray("items"); + List items = new ArrayList<>(); + for(int i = 0; i < items_array.length(); i++) { + final JSONObject item = items_array.getJSONObject(i); + items.add(new CommerceItem(item.getString("id"), item.getString("name"), item.getDouble("price"), item.getInt("quantity"))); + } + IterableApi.getInstance().trackPurchase(total, items); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + findViewById(R.id.customEvent).setOnClickListener(view -> { + EditText customEvent_edit = findViewById(R.id.customEvent_edit); + if(customEvent_edit == null) return; + Log.d("TEST_USER", String.valueOf(customEvent_edit.getText())); + + try { + JSONObject customEventItem = new JSONObject(String.valueOf(customEvent_edit.getText())); + JSONObject items = new JSONObject(customEventItem.get("dataFields").toString()); + if(customEventItem.has("eventName")) { + items.put("eventName", customEventItem.getString("eventName")); + } + IterableApi.getInstance().track("customEvent", 0, 0, items); + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + findViewById(R.id.updateUser).setOnClickListener(view -> { + EditText updateUser_edit = findViewById(R.id.updateUser_edit); + if(updateUser_edit == null) return; + Log.d("TEST_USER", String.valueOf(updateUser_edit.getText())); + + try { + JSONObject updateUserItem = new JSONObject(String.valueOf(updateUser_edit.getText())); + IterableApi.getInstance().updateUser(updateUserItem); + + } catch (JSONException e) { + throw new RuntimeException(e); + } + }); + findViewById(R.id.setUser).setOnClickListener(view -> { + EditText setUser_edit = findViewById(R.id.setUser_edit); + if(setUser_edit == null) return;; + IterableApi.getInstance().setUserId(String.valueOf(setUser_edit.getText())); + }); + findViewById(R.id.setEmail).setOnClickListener(view -> { + EditText setEmail_edit = findViewById(R.id.setEmail_edit); + if(setEmail_edit == null) return; + IterableApi.getInstance().setEmail(String.valueOf(setEmail_edit.getText())); + }); + + findViewById(R.id.btn_logout).setOnClickListener(view -> { + IterableApi.getInstance().setUserId(null); + IterableApi.getInstance().setEmail(null); + }); + + } + public void printAllSharedPreferencesData(Context context) { + SharedPreferences sharedPref = context.getSharedPreferences(IterableConstants.SHARED_PREFS_FILE, Context.MODE_PRIVATE); + Map allEntries = sharedPref.getAll(); + + for (Map.Entry entry : allEntries.entrySet()) { + Log.d("SharedPref", entry.getKey() + ": " + entry.getValue().toString()); + } + } + + @Override + public void onUnknownUserCreated(String userId) { + Log.d("userId", userId); + } + + @Override + public String onAuthTokenRequested() { + IterableApi instance = IterableApi.getInstance(); + if (instance.getAuthToken() == null) { + final String secret = "1bb125ddcda2808f118c8b5e774d341c4b03fae68ebef0d140a22da1ec0295ad24d98981fd262c93bac98fa2e63a08142c0a36fe4322c09bea90f48c161780e0"; + String userId; + String userEmail; + String jwtToken = null; + if (instance.getUserId() != null) { + userId = instance.getUserId(); // set as destination user Id + } else { + userId = null; + } + if (instance.getEmail() != null) { + userEmail = instance.getEmail(); // set as destination email Id + } else { + userEmail = null; + } + final Duration days7 = Duration.ofDays(7); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + jwtToken = IterableJwtGenerator.generateToken(secret, days7, userEmail, userId); + } + return jwtToken; + } else { + return instance.getAuthToken(); + } + } + + @Override + public void onTokenRegistrationSuccessful(String authToken) { + Log.d("Successful", authToken); + if (IterableApi.getInstance().getEmail() != null) { + Log.d("getEmail", IterableApi.getInstance().getEmail()); + } + if (IterableApi.getInstance().getUserId() != null) { + Log.d("getUserId", IterableApi.getInstance().getUserId()); + } + } + + @Override + public void onAuthFailure(AuthFailure authFailure) { + Log.d("Failure", authFailure.failureReason.toString()); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f128d96bd..904a7bf1d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" + android:id="@+id/mainLayout" tools:context="com.iterable.androidsdk.MainActivity"> + + + + + + + + + +