From a075d9737bbc7c480eaec48771ba192b58141fe3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 May 2026 01:10:09 -0700 Subject: [PATCH] Updated on 2026-08-14 --- .../domain/scanCard/chains/AnalyticsChain.kt | 3 +- .../sdk/impl/DefaultTangemSdkManager.kt | 3 +- .../core/analytics/models/AnalyticsEvent.kt | 27 +++++++++- .../com/tangem/core/analytics/Analytics.kt | 16 +++--- .../tangempay/TangemPayAnalyticsEvents.kt | 22 ++++++-- .../setup/TangemPayCardLimitSetupModel.kt | 6 +++ .../tangempay/model/TangemPayCardPageModel.kt | 7 ++- .../setup/TangemPayCardLimitSetupModelTest.kt | 54 ++++++++++++++----- 8 files changed, 107 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt index c7c56864ec..06314f32d7 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -25,9 +25,8 @@ class AnalyticsChain( val interceptor = CardContextInterceptor(previousChainResult) val params = event.params.toMutableMap() interceptor.intercept(params) - event.params = params.toMap() - Analytics.send(event) + Analytics.send(event.withParams(params.toMap())) return previousChainResult.right() } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index c4a6e936fc..797c3ce6da 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -238,9 +238,8 @@ internal class DefaultTangemSdkManager( val interceptor = CardContextInterceptor(scanResponse) val params = analyticsEvent.params.toMutableMap() interceptor.intercept(params) - analyticsEvent.params = params.toMap() - Analytics.send(event = analyticsEvent) + Analytics.send(event = analyticsEvent.withParams(params.toMap())) } .doOnFailure { tangemError -> (tangemError as? TangemSdkError)?.let { error -> diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index 8c12f046df..d259b2ba00 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -6,8 +6,31 @@ package com.tangem.core.analytics.models open class AnalyticsEvent( val category: String, val event: String, - var params: Map = mapOf(), + val params: Map = mapOf(), ) { - val id: String = "[$category] $event" + + fun withParams(newParams: Map) = AnalyticsEvent(category, event, newParams) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AnalyticsEvent + + if (category != other.category) return false + if (event != other.event) return false + if (params != other.params) return false + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + var result = category.hashCode() + result = 31 * result + event.hashCode() + result = 31 * result + params.hashCode() + result = 31 * result + id.hashCode() + return result + } } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index d6571cb78c..5b93790744 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -91,16 +91,16 @@ object Analytics : GlobalAnalyticsEventHandler { if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) { return@launch } - event.params = applyParamsInterceptors(event) - val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } + val eventWithParams = event.withParams(applyParamsInterceptors(event)) + val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(eventWithParams) } analyticsMutex.withLock { when { - eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) } - eventFilter.canBeSent(event) -> { + eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(eventWithParams) } + eventFilter.canBeSent(eventWithParams) -> { analyticsHandlers - .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } - .forEach { handler -> handler.send(event) } + .filter { handler -> eventFilter.canBeConsumedByHandler(handler, eventWithParams) } + .forEach { handler -> handler.send(eventWithParams) } } } } @@ -109,10 +109,10 @@ object Analytics : GlobalAnalyticsEventHandler { override fun sendErrorEvent(event: AnalyticsEvent) { analyticsScope.launch { - event.params = applyParamsInterceptors(event) + val eventWithParams = event.withParams(applyParamsInterceptors(event)) analyticsMutex.withLock { analyticsHandlers.filterIsInstance() - .forEach { handler -> handler.sendErrorEvent(event) } + .forEach { handler -> handler.sendErrorEvent(eventWithParams) } } } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index a8229e803a..5b3bc6f428 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -194,20 +194,36 @@ sealed class TangemPayAnalyticsEvents( ) class ReplaceCardClicked : TangemPayAnalyticsEvents( - categoryName = "Visa Screen", + categoryName = "Visa Card Management", event = "Visa Replace Card Clicked", ) class ReplaceCardConfirmationPopupOpened : TangemPayAnalyticsEvents( - categoryName = "Visa Screen", + categoryName = "Visa Card Management", event = "Visa Replace Card Confirmation Popup Opened", ) class ReplaceCardConfirmed : TangemPayAnalyticsEvents( - categoryName = "Visa Screen", + categoryName = "Visa Card Management", event = "Visa Replace Card Confirmed", ) + class LimitChangeClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Daily Limit Change Clicked", + ) + + class LimitManagementOpened : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Limit Management Screen Opened", + ) + + data class LimitChangeConfirmed(val amount: String) : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Set Limits Confirmed", + params = mapOf("amount" to amount), + ) + class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa Permanent Banner Clicked", diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt index 487e568445..ff13c3a2b7 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.tangempay.limit.setup import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,6 +21,7 @@ import com.tangem.domain.models.account.requireCardWithId import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute @@ -32,6 +34,7 @@ import java.math.BigDecimal import java.util.Currency import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ModelScoped internal class TangemPayCardLimitSetupModel @Inject constructor( @@ -41,6 +44,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, private val setTangemPayCardLimitUseCase: SetTangemPayCardLimitUseCase, private val uiMessageSender: UiMessageSender, + private val analytics: AnalyticsEventHandler, ) : Model() { private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() @@ -66,6 +70,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( ) init { + analytics.send(TangemPayAnalyticsEvents.LimitManagementOpened()) observeCardState() } @@ -130,6 +135,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( val amount = uiState.value.amountFieldModel.value.toBigDecimalOrNull() ?: return modelScope.launch { uiState.update { it.copy(isSubmitButtonLoading = true) } + analytics.send(TangemPayAnalyticsEvents.LimitChangeConfirmed(amount.toPlainString())) setTangemPayCardLimitUseCase( cardId = params.config.cardId, userWalletId = params.userWalletId, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt index 735d0b9398..14d8765c16 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt @@ -100,7 +100,7 @@ internal class TangemPayCardPageModel @Inject constructor( val symbol = getJavaCurrencyByCode(status.currencyCode).symbol fiat(status.currencyCode, symbol) }, - onChangeClick = { router.push(TangemPayCardDetailsInnerRoute.LimitSetup) }, + onChangeClick = ::onClickLimitChange, ) } else { TangemPayDailyLimitBlockState.Error @@ -132,6 +132,11 @@ internal class TangemPayCardPageModel @Inject constructor( ) } + private fun onClickLimitChange() { + analytics.send(TangemPayAnalyticsEvents.LimitChangeClicked()) + router.push(TangemPayCardDetailsInnerRoute.LimitSetup) + } + private fun onClickChangePIN(isPinSet: Boolean) { if (!isPinSet) { router.push(TangemPayCardDetailsInnerRoute.ChangePIN) diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt index f9a1defa96..6de5f4d546 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt @@ -1,6 +1,7 @@ package com.tangem.features.tangempay.limit.setup import com.google.common.truth.Truth.assertThat +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.MutableParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender @@ -15,21 +16,21 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.math.BigDecimal -@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class TangemPayCardLimitSetupModelTest { private val cardId = "test_card_id" @@ -39,6 +40,7 @@ internal class TangemPayCardLimitSetupModelTest { private val uiMessageSender: UiMessageSender = mockk(relaxed = true) private val setLimitUseCase: SetTangemPayCardLimitUseCase = mockk(relaxed = true) private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk() + private val analytics: AnalyticsEventHandler = mockk(relaxed = true) private val params = TangemPayDetailsContainerComponent.Params( userWalletId = userWalletId, @@ -90,6 +92,7 @@ internal class TangemPayCardLimitSetupModelTest { paymentAccountStatusSupplier = paymentAccountStatusSupplier, setTangemPayCardLimitUseCase = setLimitUseCase, uiMessageSender = uiMessageSender, + analytics = analytics, ) } @@ -119,7 +122,29 @@ internal class TangemPayCardLimitSetupModelTest { } @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Analytics { + @Test + fun `GIVEN model WHEN init THEN LimitManagementOpened is sent`() { + val model = createModel() + + verify(exactly = 1) { analytics.send(TangemPayAnalyticsEvents.LimitManagementOpened()) } + + model.onDestroy() + } + + @Test + fun `GIVEN model WHEN onSubmitClick THEN LimitChangeConfirmed is sent`() { + val model = createModel() + + model.uiState.value.amountFieldModel.onValueChange("100") + model.uiState.value.onSubmitClick() + verify(exactly = 1) { analytics.send(TangemPayAnalyticsEvents.LimitChangeConfirmed("100")) } + + model.onDestroy() + } + } + + @Nested inner class Presets { @Test @@ -143,14 +168,17 @@ internal class TangemPayCardLimitSetupModelTest { } } - private fun provideTestCases() = listOf( - Arguments.of("0", false), - Arguments.of("0.99", false), - Arguments.of("1", true), - Arguments.of("100", true), - Arguments.of("-1", false), - Arguments.of("", false), - Arguments.of("abc", true), - Arguments.of("1001", false), - ) + private companion object { + @JvmStatic + fun provideTestCases() = listOf( + Arguments.of("0", false), + Arguments.of("0.99", false), + Arguments.of("1", true), + Arguments.of("100", true), + Arguments.of("-1", false), + Arguments.of("", false), + Arguments.of("abc", true), + Arguments.of("1001", false), + ) + } } \ No newline at end of file