Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-07 01:10:09 -07:00
parent 7378429e90
commit a075d9737b
8 changed files with 107 additions and 31 deletions

View file

@ -25,9 +25,8 @@ class AnalyticsChain(
val interceptor = CardContextInterceptor(previousChainResult) val interceptor = CardContextInterceptor(previousChainResult)
val params = event.params.toMutableMap() val params = event.params.toMutableMap()
interceptor.intercept(params) interceptor.intercept(params)
event.params = params.toMap()
Analytics.send(event) Analytics.send(event.withParams(params.toMap()))
return previousChainResult.right() return previousChainResult.right()
} }

View file

@ -238,9 +238,8 @@ internal class DefaultTangemSdkManager(
val interceptor = CardContextInterceptor(scanResponse) val interceptor = CardContextInterceptor(scanResponse)
val params = analyticsEvent.params.toMutableMap() val params = analyticsEvent.params.toMutableMap()
interceptor.intercept(params) interceptor.intercept(params)
analyticsEvent.params = params.toMap()
Analytics.send(event = analyticsEvent) Analytics.send(event = analyticsEvent.withParams(params.toMap()))
} }
.doOnFailure { tangemError -> .doOnFailure { tangemError ->
(tangemError as? TangemSdkError)?.let { error -> (tangemError as? TangemSdkError)?.let { error ->

View file

@ -6,8 +6,31 @@ package com.tangem.core.analytics.models
open class AnalyticsEvent( open class AnalyticsEvent(
val category: String, val category: String,
val event: String, val event: String,
var params: Map<String, String> = mapOf(), val params: Map<String, String> = mapOf(),
) { ) {
val id: String = "[$category] $event" val id: String = "[$category] $event"
fun withParams(newParams: Map<String, String>) = 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
}
} }

View file

@ -91,16 +91,16 @@ object Analytics : GlobalAnalyticsEventHandler {
if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) { if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) {
return@launch return@launch
} }
event.params = applyParamsInterceptors(event) val eventWithParams = event.withParams(applyParamsInterceptors(event))
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(eventWithParams) }
analyticsMutex.withLock { analyticsMutex.withLock {
when { when {
eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) } eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(eventWithParams) }
eventFilter.canBeSent(event) -> { eventFilter.canBeSent(eventWithParams) -> {
analyticsHandlers analyticsHandlers
.filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } .filter { handler -> eventFilter.canBeConsumedByHandler(handler, eventWithParams) }
.forEach { handler -> handler.send(event) } .forEach { handler -> handler.send(eventWithParams) }
} }
} }
} }
@ -109,10 +109,10 @@ object Analytics : GlobalAnalyticsEventHandler {
override fun sendErrorEvent(event: AnalyticsEvent) { override fun sendErrorEvent(event: AnalyticsEvent) {
analyticsScope.launch { analyticsScope.launch {
event.params = applyParamsInterceptors(event) val eventWithParams = event.withParams(applyParamsInterceptors(event))
analyticsMutex.withLock { analyticsMutex.withLock {
analyticsHandlers.filterIsInstance<AnalyticsErrorHandler>() analyticsHandlers.filterIsInstance<AnalyticsErrorHandler>()
.forEach { handler -> handler.sendErrorEvent(event) } .forEach { handler -> handler.sendErrorEvent(eventWithParams) }
} }
} }
} }

View file

@ -194,20 +194,36 @@ sealed class TangemPayAnalyticsEvents(
) )
class ReplaceCardClicked : TangemPayAnalyticsEvents( class ReplaceCardClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Screen", categoryName = "Visa Card Management",
event = "Visa Replace Card Clicked", event = "Visa Replace Card Clicked",
) )
class ReplaceCardConfirmationPopupOpened : TangemPayAnalyticsEvents( class ReplaceCardConfirmationPopupOpened : TangemPayAnalyticsEvents(
categoryName = "Visa Screen", categoryName = "Visa Card Management",
event = "Visa Replace Card Confirmation Popup Opened", event = "Visa Replace Card Confirmation Popup Opened",
) )
class ReplaceCardConfirmed : TangemPayAnalyticsEvents( class ReplaceCardConfirmed : TangemPayAnalyticsEvents(
categoryName = "Visa Screen", categoryName = "Visa Card Management",
event = "Visa Replace Card Confirmed", 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( class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents(
categoryName = "Visa Onboarding", categoryName = "Visa Onboarding",
event = "Visa Permanent Banner Clicked", event = "Visa Permanent Banner Clicked",

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.limit.setup package com.tangem.features.tangempay.limit.setup
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer 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.models.pay.TangemPayCardLimitPeriod
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase 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.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute
@ -32,6 +34,7 @@ import java.math.BigDecimal
import java.util.Currency import java.util.Currency
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList")
@Stable @Stable
@ModelScoped @ModelScoped
internal class TangemPayCardLimitSetupModel @Inject constructor( internal class TangemPayCardLimitSetupModel @Inject constructor(
@ -41,6 +44,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
private val setTangemPayCardLimitUseCase: SetTangemPayCardLimitUseCase, private val setTangemPayCardLimitUseCase: SetTangemPayCardLimitUseCase,
private val uiMessageSender: UiMessageSender, private val uiMessageSender: UiMessageSender,
private val analytics: AnalyticsEventHandler,
) : Model() { ) : Model() {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -66,6 +70,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
) )
init { init {
analytics.send(TangemPayAnalyticsEvents.LimitManagementOpened())
observeCardState() observeCardState()
} }
@ -130,6 +135,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
val amount = uiState.value.amountFieldModel.value.toBigDecimalOrNull() ?: return val amount = uiState.value.amountFieldModel.value.toBigDecimalOrNull() ?: return
modelScope.launch { modelScope.launch {
uiState.update { it.copy(isSubmitButtonLoading = true) } uiState.update { it.copy(isSubmitButtonLoading = true) }
analytics.send(TangemPayAnalyticsEvents.LimitChangeConfirmed(amount.toPlainString()))
setTangemPayCardLimitUseCase( setTangemPayCardLimitUseCase(
cardId = params.config.cardId, cardId = params.config.cardId,
userWalletId = params.userWalletId, userWalletId = params.userWalletId,

View file

@ -100,7 +100,7 @@ internal class TangemPayCardPageModel @Inject constructor(
val symbol = getJavaCurrencyByCode(status.currencyCode).symbol val symbol = getJavaCurrencyByCode(status.currencyCode).symbol
fiat(status.currencyCode, symbol) fiat(status.currencyCode, symbol)
}, },
onChangeClick = { router.push(TangemPayCardDetailsInnerRoute.LimitSetup) }, onChangeClick = ::onClickLimitChange,
) )
} else { } else {
TangemPayDailyLimitBlockState.Error 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) { private fun onClickChangePIN(isPinSet: Boolean) {
if (!isPinSet) { if (!isPinSet) {
router.push(TangemPayCardDetailsInnerRoute.ChangePIN) router.push(TangemPayCardDetailsInnerRoute.ChangePIN)

View file

@ -1,6 +1,7 @@
package com.tangem.features.tangempay.limit.setup package com.tangem.features.tangempay.limit.setup
import com.google.common.truth.Truth.assertThat 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.model.MutableParamsContainer
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender 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.TangemPayDetailsConfig
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
import java.math.BigDecimal import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class TangemPayCardLimitSetupModelTest { internal class TangemPayCardLimitSetupModelTest {
private val cardId = "test_card_id" private val cardId = "test_card_id"
@ -39,6 +40,7 @@ internal class TangemPayCardLimitSetupModelTest {
private val uiMessageSender: UiMessageSender = mockk(relaxed = true) private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
private val setLimitUseCase: SetTangemPayCardLimitUseCase = mockk(relaxed = true) private val setLimitUseCase: SetTangemPayCardLimitUseCase = mockk(relaxed = true)
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk() private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
private val params = TangemPayDetailsContainerComponent.Params( private val params = TangemPayDetailsContainerComponent.Params(
userWalletId = userWalletId, userWalletId = userWalletId,
@ -90,6 +92,7 @@ internal class TangemPayCardLimitSetupModelTest {
paymentAccountStatusSupplier = paymentAccountStatusSupplier, paymentAccountStatusSupplier = paymentAccountStatusSupplier,
setTangemPayCardLimitUseCase = setLimitUseCase, setTangemPayCardLimitUseCase = setLimitUseCase,
uiMessageSender = uiMessageSender, uiMessageSender = uiMessageSender,
analytics = analytics,
) )
} }
@ -119,7 +122,29 @@ internal class TangemPayCardLimitSetupModelTest {
} }
@Nested @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 { inner class Presets {
@Test @Test
@ -143,14 +168,17 @@ internal class TangemPayCardLimitSetupModelTest {
} }
} }
private fun provideTestCases() = listOf( private companion object {
Arguments.of("0", false), @JvmStatic
Arguments.of("0.99", false), fun provideTestCases() = listOf(
Arguments.of("1", true), Arguments.of("0", false),
Arguments.of("100", true), Arguments.of("0.99", false),
Arguments.of("-1", false), Arguments.of("1", true),
Arguments.of("", false), Arguments.of("100", true),
Arguments.of("abc", true), Arguments.of("-1", false),
Arguments.of("1001", false), Arguments.of("", false),
) Arguments.of("abc", true),
Arguments.of("1001", false),
)
}
} }