Updated on 2026-08-14
This commit is contained in:
parent
7378429e90
commit
a075d9737b
8 changed files with 107 additions and 31 deletions
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue