From d57fbfa8564e6e089be52b013d0327bb3d7fa99c Mon Sep 17 00:00:00 2001 From: Cristian Dregan Date: Tue, 3 Dec 2024 17:26:54 +0200 Subject: [PATCH] Release 1.8.0 --- OmetriaSDK/build.gradle.kts | 2 +- .../com/android/ometriasdk/core/Constants.kt | 3 +- .../com/android/ometriasdk/core/LocalCache.kt | 21 ++++++++++ .../com/android/ometriasdk/core/Ometria.kt | 42 +++++++++++++++---- .../com/android/ometriasdk/core/Repository.kt | 19 ++++++++- .../ometriasdk/core/event/EventHandler.kt | 36 +++++----------- README.md | 33 +++++++++++++-- app/build.gradle.kts | 4 +- .../java/com/android/sample/data/EventType.kt | 4 +- .../sample/presentation/HomeFragment.kt | 34 ++++++++++++--- app/src/main/res/layout/fragment_home.xml | 27 +++++++++++- app/src/main/res/values/strings.xml | 4 +- 12 files changed, 180 insertions(+), 49 deletions(-) diff --git a/OmetriaSDK/build.gradle.kts b/OmetriaSDK/build.gradle.kts index 3433517..da9c7f0 100644 --- a/OmetriaSDK/build.gradle.kts +++ b/OmetriaSDK/build.gradle.kts @@ -1,4 +1,4 @@ -val versionName = "1.7.1" +val versionName = "1.8.0" plugins { id("com.android.library") diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Constants.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Constants.kt index 6e925c2..ee14c63 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Constants.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Constants.kt @@ -9,6 +9,7 @@ internal object Constants { const val PAGE = "page" const val EMAIL = "email" const val CUSTOMER_ID = "customerId" + const val STORE_ID = "storeId" const val PRODUCT_ID = "productId" const val BASKET = "basket" const val ORDER_ID = "orderId" @@ -46,4 +47,4 @@ internal object Constants { const val EVENTS = "Events" const val GENERAL = "General" } -} \ No newline at end of file +} diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/LocalCache.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/LocalCache.kt index 42690b9..838fb52 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/LocalCache.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/LocalCache.kt @@ -17,6 +17,7 @@ private const val EVENTS_KEY = "EVENTS_KEY" private const val PUSH_TOKEN_KEY = "PUSH_TOKEN_KEY" private const val CUSTOMER_ID_KEY = "CUSTOMER_ID_KEY" private const val EMAIL_KEY = "EMAIL_KEY" +private const val STORE_ID_KEY = "STORE_ID_KEY" private const val ARE_NOTIFICATIONS_ENABLED_KEY = "ARE_NOTIFICATIONS_ENABLED_KEY" private const val IS_FIRST_PERMISSION_UPDATE_EVENT_KEY = "IS_FIRST_PERMISSION_UPDATE_EVENT_KEY" private const val JSON_ARRAY = "[]" @@ -201,11 +202,31 @@ internal class LocalCache(private val context: Context) { return localCacheEncryptedPreferences.getString(EMAIL_KEY, null) } + fun saveStoreId(storeId: String?) { + synchronized(this) { + try { + if (storeId == null) { + localCacheEncryptedPreferences.edit().remove(STORE_ID_KEY).apply() + return + } + + localCacheEncryptedPreferences.edit().putString(STORE_ID_KEY, storeId).apply() + } catch (e: Exception) { + Logger.e(CACHE, e.message ?: "Failed to save storeId") + } + } + } + + fun getStoreId(): String? { + return localCacheEncryptedPreferences.getString(STORE_ID_KEY, null) + } + fun clearProfileIdentifiedData() { synchronized(this) { try { localCacheEncryptedPreferences.edit().remove(CUSTOMER_ID_KEY).apply() localCacheEncryptedPreferences.edit().remove(EMAIL_KEY).apply() + localCacheEncryptedPreferences.edit().remove(STORE_ID_KEY).apply() } catch (e: Exception) { Logger.e(CACHE, e.message ?: "Failed to clear profile identified data") } diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt index a64bbf2..a2054e7 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Ometria.kt @@ -26,6 +26,7 @@ import com.android.ometriasdk.core.Constants.Params.PAGE import com.android.ometriasdk.core.Constants.Params.PRODUCT_ID import com.android.ometriasdk.core.Constants.Params.PROPERTIES import com.android.ometriasdk.core.Constants.Params.PUSH_TOKEN +import com.android.ometriasdk.core.Constants.Params.STORE_ID import com.android.ometriasdk.core.event.EventHandler import com.android.ometriasdk.core.event.OmetriaBasket import com.android.ometriasdk.core.event.OmetriaEventType @@ -249,12 +250,31 @@ class Ometria private constructor() : OmetriaNotificationInteractionHandler { } } - fun onNewToken(token: String) { - if (localCache.getPushToken() != token) { + // ToDo Remove forceRefresh parameter before release + fun onNewToken(token: String, forceRefresh: Boolean = false) { + if (localCache.getPushToken() != token || forceRefresh) { trackPushTokenRefreshedEvent(token) } } + /** + * Updates the store identifier for the current user. + * + * @param storeId: The string representing the store identifier. + */ + fun updateStoreId(storeId: String?) { + repository.saveStoreId(storeId) + trackProfileIdentifiedEvent() + } + + private fun trackProfileIdentifiedEvent() { + val data = mutableMapOf() + repository.getEmail()?.let { data[EMAIL] = it } + repository.getCustomerId()?.let { data[CUSTOMER_ID] = it } + repository.getStoreId()?.let { data[STORE_ID] = it } + trackEvent(OmetriaEventType.PROFILE_IDENTIFIED, data) + } + private fun trackEvent(type: OmetriaEventType, data: Map? = null) { eventHandler.processEvent(type, data?.toMutableMap()) } @@ -311,9 +331,13 @@ class Ometria private constructor() : OmetriaNotificationInteractionHandler { * by e-mail event: send either event as soon as you have the information, for optimal integration. * * @param customerId The ID reserved for a particular user in your database. + * @param storeId: The string representing the store identifier. */ - fun trackProfileIdentifiedByCustomerIdEvent(customerId: String) { - trackEvent(OmetriaEventType.PROFILE_IDENTIFIED, mapOf(CUSTOMER_ID to customerId)) + fun trackProfileIdentifiedByCustomerIdEvent(customerId: String, storeId: String? = null) { + val data = mutableMapOf(CUSTOMER_ID to customerId) + storeId?.let { data[STORE_ID] = it } + repository.cacheProfileIdentifiedData(data) + trackProfileIdentifiedEvent() } /** @@ -322,9 +346,13 @@ class Ometria private constructor() : OmetriaNotificationInteractionHandler { * an profile identified by customerId event: send either event as soon as you have the information, for optimal integration. * * @param email: The email by which you identify a particular user in your database. + * @param storeId: The string representing the store identifier. */ - fun trackProfileIdentifiedByEmailEvent(email: String) { - trackEvent(OmetriaEventType.PROFILE_IDENTIFIED, mapOf(EMAIL to email)) + fun trackProfileIdentifiedByEmailEvent(email: String, storeId: String? = null) { + val data = mutableMapOf(EMAIL to email) + storeId?.let { data[STORE_ID] = it } + repository.cacheProfileIdentifiedData(data) + trackProfileIdentifiedEvent() } /** @@ -536,4 +564,4 @@ class Ometria private constructor() : OmetriaNotificationInteractionHandler { trackDeepLinkOpenedEvent(safeDeeplinkActionUrl, "Browser") } } -} \ No newline at end of file +} diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt index 9383059..63a832d 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/Repository.kt @@ -3,6 +3,9 @@ package com.android.ometriasdk.core import android.os.Handler import android.os.Looper import com.android.ometriasdk.core.Constants.Logger.NETWORK +import com.android.ometriasdk.core.Constants.Params.CUSTOMER_ID +import com.android.ometriasdk.core.Constants.Params.EMAIL +import com.android.ometriasdk.core.Constants.Params.STORE_ID import com.android.ometriasdk.core.event.OmetriaEvent import com.android.ometriasdk.core.event.toApiRequest import com.android.ometriasdk.core.listener.ProcessAppLinkListener @@ -103,8 +106,22 @@ internal class Repository( localCache.saveEmail(email) } + fun getStoreId(): String? = localCache.getStoreId() + + fun saveStoreId(storeId: String?) { + localCache.saveStoreId(storeId) + } + fun getEmail(): String? = localCache.getEmail() + fun cacheProfileIdentifiedData(data: Map?) { + data?.let { + it[CUSTOMER_ID]?.let { customerId -> saveCustomerId(customerId as String) } + it[EMAIL]?.let { email -> saveEmail(email as String) } + it[STORE_ID]?.let { storeId -> saveStoreId(storeId as String) } + } + } + fun clearProfileIdentifiedData() { localCache.clearProfileIdentifiedData() } @@ -147,4 +164,4 @@ internal class Repository( } fun getSdkVersionRN(): String? = localCache.getSdkVersionRN() -} \ No newline at end of file +} diff --git a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/event/EventHandler.kt b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/event/EventHandler.kt index c2a891e..129d0c1 100644 --- a/OmetriaSDK/src/main/java/com/android/ometriasdk/core/event/EventHandler.kt +++ b/OmetriaSDK/src/main/java/com/android/ometriasdk/core/event/EventHandler.kt @@ -2,11 +2,13 @@ package com.android.ometriasdk.core.event import android.content.Context import androidx.core.content.pm.PackageInfoCompat -import com.android.ometriasdk.core.Constants +import com.android.ometriasdk.core.Constants.Date.API_DATE_FORMAT import com.android.ometriasdk.core.Constants.Logger.EVENTS import com.android.ometriasdk.core.Constants.Logger.NETWORK import com.android.ometriasdk.core.Constants.Params.CUSTOMER_ID import com.android.ometriasdk.core.Constants.Params.EMAIL +import com.android.ometriasdk.core.Constants.Params.PUSH_TOKEN +import com.android.ometriasdk.core.Constants.Params.STORE_ID import com.android.ometriasdk.core.Logger import com.android.ometriasdk.core.Ometria import com.android.ometriasdk.core.Repository @@ -27,7 +29,7 @@ private const val NO_VALUE = -1L */ internal class EventHandler(context: Context, private val repository: Repository) { private val dateFormat: DateFormat = - SimpleDateFormat(Constants.Date.API_DATE_FORMAT, Locale.UK) + SimpleDateFormat(API_DATE_FORMAT, Locale.UK) private val appId = context.packageName private val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) private var syncTimestamp: Long = NO_VALUE @@ -42,15 +44,10 @@ internal class EventHandler(context: Context, private val repository: Repository if (type == OmetriaEventType.PUSH_TOKEN_REFRESHED) { data?.let { - repository.savePushToken(it[Constants.Params.PUSH_TOKEN] as String) - - repository.getCustomerId()?.let { customerId -> - data[CUSTOMER_ID] = customerId - } - - repository.getEmail()?.let { customerId -> - data[EMAIL] = customerId - } + repository.savePushToken(it[PUSH_TOKEN] as String) + repository.getCustomerId()?.let { customerId -> data[CUSTOMER_ID] = customerId } + repository.getEmail()?.let { email -> data[EMAIL] = email } + repository.getStoreId()?.let { storeId -> data[STORE_ID] = storeId } } } @@ -70,22 +67,10 @@ internal class EventHandler(context: Context, private val repository: Repository when (event.type) { OmetriaEventType.PROFILE_IDENTIFIED.id -> { - cacheProfileIdentifiedData(data) Ometria.instance().trackPushTokenRefreshedEvent(repository.getPushToken()) } - OmetriaEventType.PROFILE_DEIDENTIFIED.id -> { - repository.clearProfileIdentifiedData() - } - } - } - private fun cacheProfileIdentifiedData(data: Map?) { - data?.let { - if (it[CUSTOMER_ID] != null) { - repository.saveCustomerId(it[CUSTOMER_ID] as String) - } else { - repository.saveEmail(it[EMAIL] as String) - } + OmetriaEventType.PROFILE_DEIDENTIFIED.id -> repository.clearProfileIdentifiedData() } } @@ -98,6 +83,7 @@ internal class EventHandler(context: Context, private val repository: Repository OmetriaEventType.APP_FOREGROUNDED.id, OmetriaEventType.APP_BACKGROUNDED.id, OmetriaEventType.NOTIFICATION_RECEIVED.id -> flushEvents() + else -> flushEventsIfNeeded() } } @@ -153,4 +139,4 @@ internal class EventHandler(context: Context, private val repository: Repository */ private fun shouldFlush(): Boolean = repository.getEvents().filter { !it.isBeingFlushed }.size >= FLUSH_LIMIT -} \ No newline at end of file +} diff --git a/README.md b/README.md index 4134253..02dae19 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ To install the library inside **Android Studio**, declare it as dependency in yo ```gradle dependencies { - implementation 'com.ometria:android-sdk:1.7.1' + implementation 'com.ometria:android-sdk:1.8.0' } ``` @@ -127,7 +127,7 @@ Ometria.instance().trackBasketUpdatedEvent(basket = basket) An app user has just identified themselves, i.e. logged in. ```kotlin -trackProfileIdentifiedByCustomerIdEvent(customerId: String) +trackProfileIdentifiedByCustomerIdEvent(customerId: String, storeId: String? = null) ``` Their **customer ID** is their **user ID** in your database. @@ -135,7 +135,7 @@ Their **customer ID** is their **user ID** in your database. Sometimes a user only supplies their email address without fully logging in or having an account. In that case, Ometria can profile match based on email: ```kotlin -trackProfileIdentifiedByEmailEvent(email: String) +trackProfileIdentifiedByEmailEvent(email: String, storeId: String? = null) ``` Having a **customerId** makes profile matching more robust. @@ -154,6 +154,33 @@ Use this if a user logs out, or otherwise signals that this device is no longer trackProfileDeidentifiedEvent() ``` +#### Update store identifier + +Ometria supports multiple stores for the same ecommerce platform (e.g. separate stores for different countries). +There are three different ways to update the store identifier: + +1. Using an optional parameter in the `profileIdentified` events tracking methods + +```kotlin +trackProfileIdentifiedByCustomerIdEvent(customerId: String, storeId: String? = null) +trackProfileIdentifiedByEmailEvent(email: String, storeId: String? = null) +``` + +When omitting the `storeId` parameter, or providing a `null` value, the store identifier will not be affected in any way. Only sending a valid, non-null parameter will cause the store identifier to be updated to that value. + +2. Using the dedicated method that allows setting/resetting the store identifier + +```kotlin +updateStoreId(storeId: String?) +``` + +* with a null `storeId` parameter, the method resets the store identifier. +* with a non-null `storeId` parameter, the method sets the store identifier to the provided value. + +3. Using the `profileDeidentified` event +Tracking a profile deidentified event, will reset the `customerId`, the `email`, and the `storeId` for the current app installment. + + #### Product Viewed A visitor clicks / taps / views / highlights or otherwise shows interest in a product. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4c3e19b..66d62ad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "com.android.sample" minSdk = 23 targetSdk = 33 - versionCode = 12 - versionName = "1.3.0" + versionCode = 14 + versionName = "1.4.1" } signingConfigs { getByName("debug") { diff --git a/app/src/main/java/com/android/sample/data/EventType.kt b/app/src/main/java/com/android/sample/data/EventType.kt index 4030107..2f82dcc 100644 --- a/app/src/main/java/com/android/sample/data/EventType.kt +++ b/app/src/main/java/com/android/sample/data/EventType.kt @@ -17,6 +17,8 @@ enum class EventType { ORDER_COMPLETED, HOME_SCREEN_VIEWED, CUSTOM, + SIMULATE_PUSH_TOKEN_REFRESHED, + RESET_STORE_ID, FLUSH, CLEAR -} \ No newline at end of file +} diff --git a/app/src/main/java/com/android/sample/presentation/HomeFragment.kt b/app/src/main/java/com/android/sample/presentation/HomeFragment.kt index 4c8e225..99d3f57 100644 --- a/app/src/main/java/com/android/sample/presentation/HomeFragment.kt +++ b/app/src/main/java/com/android/sample/presentation/HomeFragment.kt @@ -22,6 +22,7 @@ import com.android.ometriasdk.core.event.OmetriaBasketItem import com.android.sample.data.AppPreferencesUtils import com.android.sample.data.EventType import com.android.sample.databinding.FragmentHomeBinding +import com.google.firebase.messaging.FirebaseMessaging private const val POSITION_KEY = "position_key" const val TAB_ONE = 0 @@ -76,6 +77,8 @@ class HomeFragment : Fragment() { binding.loginWithEmailBTN.isVisible = screenPosition == TAB_ONE binding.customerIdET.isVisible = screenPosition == TAB_ONE binding.loginWithCustomerIdBTN.isVisible = screenPosition == TAB_ONE + binding.storeIdET.isVisible = screenPosition == TAB_ONE + binding.setStoreIdBTN.isVisible = screenPosition == TAB_ONE binding.titleTV.isVisible = screenPosition == TAB_ONE && !ometriaNotificationString.isNullOrEmpty() @@ -96,14 +99,17 @@ class HomeFragment : Fragment() { showEnterApiTokenDialog() } binding.loginWithEmailBTN.setOnClickListener { + val storeId = binding.storeIdET.text.toString() val email = binding.emailET.text.toString() - Ometria.instance().trackProfileIdentifiedByEmailEvent(email) - Ometria.instance().flush() + Ometria.instance().trackProfileIdentifiedByEmailEvent(email, storeId) } binding.loginWithCustomerIdBTN.setOnClickListener { + val storeId = binding.storeIdET.text.toString() val customerId = binding.customerIdET.text.toString() - Ometria.instance().trackProfileIdentifiedByCustomerIdEvent(customerId) - Ometria.instance().flush() + Ometria.instance().trackProfileIdentifiedByCustomerIdEvent(customerId, storeId) + } + binding.setStoreIdBTN.setOnClickListener { + Ometria.instance().updateStoreId(binding.storeIdET.text.toString()) } } @@ -121,30 +127,48 @@ class HomeFragment : Fragment() { when (eventType) { EventType.SCREEN_VIEWED -> Ometria.instance() .trackScreenViewedEvent("TestScreenName") + EventType.PROFILE_IDENTIFIED_BY_EMAIL -> Ometria.instance() .trackProfileIdentifiedByEmailEvent("test@gmail.com") + EventType.PROFILE_IDENTIFIED_BY_CUSTOMER_ID -> Ometria.instance() .trackProfileIdentifiedByCustomerIdEvent("test_customer_id") + EventType.PROFILE_DEIDENTIFIED -> Ometria.instance() .trackProfileDeidentifiedEvent() + EventType.PRODUCT_VIEWED -> Ometria.instance() .trackProductViewedEvent("product_1") + EventType.PRODUCT_LISTING_VIEWED -> Ometria.instance().trackProductListingViewedEvent( "search", mapOf("searchQuery" to "some search terms") ) + EventType.BASKET_VIEWED -> Ometria.instance() .trackBasketViewedEvent() + EventType.BASKET_UPDATED -> Ometria.instance() .trackBasketUpdatedEvent(getBasket()) + EventType.CHECKOUT_STARTED -> Ometria.instance() .trackCheckoutStartedEvent("orderId_1") + EventType.ORDER_COMPLETED -> Ometria.instance() .trackOrderCompletedEvent("orderId_1", getBasket()) + EventType.HOME_SCREEN_VIEWED -> Ometria.instance() .trackHomeScreenViewedEvent() + EventType.CUSTOM -> Ometria.instance() .trackCustomEvent("my_custom_type", mapOf(Pair("param_key", "param_value"))) + + EventType.SIMULATE_PUSH_TOKEN_REFRESHED -> FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> + Ometria.instance().onNewToken(task.result, true) + } + + EventType.RESET_STORE_ID -> Ometria.instance().updateStoreId(null) + EventType.FLUSH -> Ometria.instance().flush() EventType.CLEAR -> Ometria.instance().clear() } @@ -226,4 +250,4 @@ class HomeFragment : Fragment() { Toast.LENGTH_LONG ).show() } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index adb2560..8562d3f 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -59,6 +59,29 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/detailsBTN" /> + + +