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 params = event.params.toMutableMap()
interceptor.intercept(params)
event.params = params.toMap()
Analytics.send(event)
Analytics.send(event.withParams(params.toMap()))
return previousChainResult.right()
}

View file

@ -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 ->

View file

@ -6,8 +6,31 @@ package com.tangem.core.analytics.models
open class AnalyticsEvent(
val category: String,
val event: String,
var params: Map<String, String> = mapOf(),
val params: Map<String, String> = mapOf(),
) {
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)) {
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<AnalyticsErrorHandler>()
.forEach { handler -> handler.sendErrorEvent(event) }
.forEach { handler -> handler.sendErrorEvent(eventWithParams) }
}
}
}

View file

@ -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",

View file

@ -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,

View file

@ -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)

View file

@ -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,7 +168,9 @@ internal class TangemPayCardLimitSetupModelTest {
}
}
private fun provideTestCases() = listOf(
private companion object {
@JvmStatic
fun provideTestCases() = listOf(
Arguments.of("0", false),
Arguments.of("0.99", false),
Arguments.of("1", true),
@ -154,3 +181,4 @@ internal class TangemPayCardLimitSetupModelTest {
Arguments.of("1001", false),
)
}
}