Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-08 14:20:04 +05:00
parent 03f702640e
commit abf6fc7d69
11 changed files with 129 additions and 11 deletions

View file

@ -42,6 +42,10 @@ sealed class MainScreenAnalyticsEvent(
event = "Button - Add Funds", event = "Button - Add Funds",
) )
class ButtonTransfer : MainScreenAnalyticsEvent(
event = "Button - Transfer",
)
class LimitsClicked : MainScreenAnalyticsEvent( class LimitsClicked : MainScreenAnalyticsEvent(
event = "Limits Clicked", event = "Limits Clicked",
) )

View file

@ -0,0 +1,23 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
sealed class TransferAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = "Transfer", event = event, params = params) {
class MethodScreenOpened(source: AnalyticsParam.ScreensSources) : TransferAnalyticsEvent(
event = "Method Screen Opened",
params = mapOf(AnalyticsParam.SOURCE to source.value),
)
class ButtonSell : TransferAnalyticsEvent(event = "Button - Sell")
class ButtonSwap : TransferAnalyticsEvent(event = "Button - Swap")
class ButtonSend : TransferAnalyticsEvent(event = "Button - Send")
class ButtonSwapAndSend : TransferAnalyticsEvent(event = "Button - Swap&Send")
}

View file

@ -136,6 +136,30 @@ sealed class TokenScreenAnalyticsEvent(
status = status, status = status,
blockchain = blockchain, blockchain = blockchain,
) )
class ButtonAddFunds(
token: String,
blockchain: String,
derivationIndex: Int? = null,
) : ButtonWithParams(
event = "Button - Add Funds",
token = token,
status = null,
blockchain = blockchain,
derivationIndex = derivationIndex,
)
class ButtonTransfer(
token: String,
blockchain: String,
derivationIndex: Int? = null,
) : ButtonWithParams(
event = "Button - Transfer",
token = token,
status = null,
blockchain = blockchain,
derivationIndex = derivationIndex,
)
} }
class ActionButtonDisabled( class ActionButtonDisabled(

View file

@ -8,9 +8,9 @@ internal sealed class ManageFundsAnalyticsEvent(
params: Map<String, String> = emptyMap(), params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = CATEGORY, event = event, params = params) { ) : AnalyticsEvent(category = CATEGORY, event = event, params = params) {
class MethodScreenOpened(source: String) : ManageFundsAnalyticsEvent( class MethodScreenOpened(source: AnalyticsParam.ScreensSources) : ManageFundsAnalyticsEvent(
event = "Method Screen Opened", event = "Method Screen Opened",
params = mapOf(AnalyticsParam.SOURCE to source), params = mapOf(AnalyticsParam.SOURCE to source.value),
) )
class ButtonBuy : ManageFundsAnalyticsEvent(event = "Button - Buy") class ButtonBuy : ManageFundsAnalyticsEvent(event = "Button - Buy")
@ -21,6 +21,5 @@ internal sealed class ManageFundsAnalyticsEvent(
companion object { companion object {
private const val CATEGORY = "Add Funds" private const val CATEGORY = "Add Funds"
const val SOURCE_MAIN_SCREEN = "Main Screen"
} }
} }

View file

@ -7,6 +7,8 @@ import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.markets.action.CryptoCurrencyData import com.tangem.common.ui.markets.action.CryptoCurrencyData
import com.tangem.common.ui.markets.action.TokenActionsBSContentUM import com.tangem.common.ui.markets.action.TokenActionsBSContentUM
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.TransferAnalyticsEvent
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
@ -150,11 +152,20 @@ internal class ManageFundsModel @Inject constructor(
} }
override fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) { override fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) {
val event = when (action) { val event = when (flowType) {
TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy() ManageFundsComponent.FlowType.AddFunds -> when (action) {
TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap() TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy()
TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive() TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap()
else -> null TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive()
else -> null
}
ManageFundsComponent.FlowType.Transfer -> when (action) {
TokenActionsBSContentUM.Action.Send -> TransferAnalyticsEvent.ButtonSend()
TokenActionsBSContentUM.Action.Exchange -> TransferAnalyticsEvent.ButtonSwap()
TokenActionsBSContentUM.Action.SendWithSwap -> TransferAnalyticsEvent.ButtonSwapAndSend()
TokenActionsBSContentUM.Action.Sell -> TransferAnalyticsEvent.ButtonSell()
else -> null
}
} }
event?.let { analyticsEventHandler.send(it) } event?.let { analyticsEventHandler.send(it) }
if (shouldDismiss) { if (shouldDismiss) {
@ -178,9 +189,7 @@ internal class ManageFundsModel @Inject constructor(
private fun initChooseToken(mode: ManageFundsComponent.LaunchMode.ChooseToken) { private fun initChooseToken(mode: ManageFundsComponent.LaunchMode.ChooseToken) {
chooseTokenBridge.selectWalletTab(mode.userWalletId) chooseTokenBridge.selectWalletTab(mode.userWalletId)
analyticsEventHandler.send( sendMethodScreenOpenedEvent()
ManageFundsAnalyticsEvent.MethodScreenOpened(source = ManageFundsAnalyticsEvent.SOURCE_MAIN_SCREEN),
)
replaceRoot(UiRoute.ChooseToken) replaceRoot(UiRoute.ChooseToken)
modelScope.launch { modelScope.launch {
chooseTokenBridge.onCurrencyChosen.receiveAsFlow().collect(::openTokenActionsFromBridge) chooseTokenBridge.onCurrencyChosen.receiveAsFlow().collect(::openTokenActionsFromBridge)
@ -209,6 +218,7 @@ internal class ManageFundsModel @Inject constructor(
params.onDismiss() params.onDismiss()
return@launch return@launch
} }
sendMethodScreenOpenedEvent()
tokenActionsTrigger.value = TokenActionsRequest(wallet, match.first, match.second) tokenActionsTrigger.value = TokenActionsRequest(wallet, match.first, match.second)
replaceRoot(tokenActionsRoute(match.second)) replaceRoot(tokenActionsRoute(match.second))
} }
@ -217,6 +227,9 @@ internal class ManageFundsModel @Inject constructor(
private fun initFilteredByRawId(mode: ManageFundsComponent.LaunchMode.FilteredByRawId) { private fun initFilteredByRawId(mode: ManageFundsComponent.LaunchMode.FilteredByRawId) {
modelScope.launch { modelScope.launch {
val entries = collectFilteredEntries(mode.rawCurrencyId) val entries = collectFilteredEntries(mode.rawCurrencyId)
if (entries.isNotEmpty()) {
sendMethodScreenOpenedEvent()
}
when (entries.size) { when (entries.size) {
0 -> params.onDismiss() 0 -> params.onDismiss()
1 -> { 1 -> {
@ -250,6 +263,19 @@ internal class ManageFundsModel @Inject constructor(
} }
} }
private fun sendMethodScreenOpenedEvent() {
val source = when (launchMode) {
is ManageFundsComponent.LaunchMode.ChooseToken -> AnalyticsParam.ScreensSources.Main
is ManageFundsComponent.LaunchMode.TokenActionsOnly -> AnalyticsParam.ScreensSources.Token
is ManageFundsComponent.LaunchMode.FilteredByRawId -> AnalyticsParam.ScreensSources.Market
}
val event = when (flowType) {
ManageFundsComponent.FlowType.AddFunds -> ManageFundsAnalyticsEvent.MethodScreenOpened(source = source)
ManageFundsComponent.FlowType.Transfer -> TransferAnalyticsEvent.MethodScreenOpened(source = source)
}
analyticsEventHandler.send(event)
}
private fun openTokenActionsFromBridge(result: ChooseTokenResult) { private fun openTokenActionsFromBridge(result: ChooseTokenResult) {
val account = result.account as? AccountStatus.CryptoPortfolio ?: return val account = result.account as? AccountStatus.CryptoPortfolio ?: return
openTokenActions( openTokenActions(

View file

@ -393,6 +393,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
} }
fun openAddFunds(rawCurrencyId: com.tangem.domain.models.currency.CryptoCurrency.RawID) { fun openAddFunds(rawCurrencyId: com.tangem.domain.models.currency.CryptoCurrency.RawID) {
analyticsEventHandler.send(analyticsEventBuilder.addFundsClicked())
addFundsSheetNavigation.activate(AddFundsSlotRoute(rawCurrencyId = rawCurrencyId)) addFundsSheetNavigation.activate(AddFundsSlotRoute(rawCurrencyId = rawCurrencyId))
} }

View file

@ -70,6 +70,11 @@ internal class MarketDetailsAnalyticsEvent(
event = "Button - Share", event = "Button - Share",
params = mapOf("Token" to token.symbol), params = mapOf("Token" to token.symbol),
) )
fun addFundsClicked() = MarketDetailsAnalyticsEvent(
event = "Button - Add Funds",
params = mapOf("Token" to token.symbol),
)
} }
enum class IntervalType(val source: String) { enum class IntervalType(val source: String) {

View file

@ -20,6 +20,7 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
import com.tangem.core.analytics.models.event.TransferAnalyticsEvent
import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.GlobalUiMessageSender
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
@ -368,6 +369,7 @@ internal class TokenDetailsModel @Inject constructor(
actions = state.states, actions = state.states,
networkSource = networkSource, networkSource = networkSource,
clickIntents = this@TokenDetailsModel, clickIntents = this@TokenDetailsModel,
analyticsEventHandler = analyticsEventsHandler,
onActionDispatched = bottomSheetNavigation::dismiss, onActionDispatched = bottomSheetNavigation::dismiss,
), ),
) )
@ -547,6 +549,13 @@ internal class TokenDetailsModel @Inject constructor(
} }
override fun onAddFundsClick() { override fun onAddFundsClick() {
analyticsEventsHandler.send(
TokenScreenAnalyticsEvent.ButtonWithParams.ButtonAddFunds(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
derivationIndex = getAccountIndexOrNull(),
),
)
bottomSheetNavigation.activate( bottomSheetNavigation.activate(
TokenDetailsBottomSheetConfig.AddFunds( TokenDetailsBottomSheetConfig.AddFunds(
userWalletId = userWalletId, userWalletId = userWalletId,
@ -556,6 +565,13 @@ internal class TokenDetailsModel @Inject constructor(
} }
override fun onTransferClick() { override fun onTransferClick() {
analyticsEventsHandler.send(
TokenScreenAnalyticsEvent.ButtonWithParams.ButtonTransfer(
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
derivationIndex = getAccountIndexOrNull(),
),
)
val amount = cryptoCurrencyStatus?.value?.amount val amount = cryptoCurrencyStatus?.value?.amount
if (amount == null || amount.signum() <= 0) { if (amount == null || amount.signum() <= 0) {
handleUnavailabilityReason( handleUnavailabilityReason(
@ -565,6 +581,9 @@ internal class TokenDetailsModel @Inject constructor(
) )
return return
} }
analyticsEventsHandler.send(
TransferAnalyticsEvent.MethodScreenOpened(source = AnalyticsParam.ScreensSources.Token),
)
bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.Transfer) bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.Transfer)
} }

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TransferAnalyticsEvent
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.TokenActionsState
@ -13,6 +15,7 @@ internal class UpdateTransferTransformer(
private val actions: List<TokenActionsState.ActionState>, private val actions: List<TokenActionsState.ActionState>,
private val networkSource: StatusSource, private val networkSource: StatusSource,
private val clickIntents: TokenDetailsClickIntents, private val clickIntents: TokenDetailsClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val onActionDispatched: () -> Unit, private val onActionDispatched: () -> Unit,
) : Transformer<TokenDetailsUM> { ) : Transformer<TokenDetailsUM> {
@ -28,6 +31,7 @@ internal class UpdateTransferTransformer(
isLoading = action.unavailabilityReason.isOutdatedLoading(), isLoading = action.unavailabilityReason.isOutdatedLoading(),
isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None,
onClick = { onClick = {
analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSend())
onActionDispatched() onActionDispatched()
clickIntents.onSendClick(action.unavailabilityReason) clickIntents.onSendClick(action.unavailabilityReason)
}, },
@ -38,6 +42,7 @@ internal class UpdateTransferTransformer(
isLoading = action.unavailabilityReason.isLoading, isLoading = action.unavailabilityReason.isLoading,
isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None,
onClick = { onClick = {
analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSwap())
onActionDispatched() onActionDispatched()
clickIntents.onSwapFromClick(action.unavailabilityReason) clickIntents.onSwapFromClick(action.unavailabilityReason)
}, },
@ -53,6 +58,7 @@ internal class UpdateTransferTransformer(
isLoading = false, isLoading = false,
isEnabled = true, isEnabled = true,
onClick = { onClick = {
analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSwapAndSend())
onActionDispatched() onActionDispatched()
clickIntents.onSwapAndSendClick(it.unavailabilityReason) clickIntents.onSwapAndSendClick(it.unavailabilityReason)
}, },
@ -63,6 +69,7 @@ internal class UpdateTransferTransformer(
isLoading = action.unavailabilityReason.isOutdatedLoading(), isLoading = action.unavailabilityReason.isOutdatedLoading(),
isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None,
onClick = { onClick = {
analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSell())
onActionDispatched() onActionDispatched()
clickIntents.onSellClick(action.unavailabilityReason) clickIntents.onSellClick(action.unavailabilityReason)
}, },

View file

@ -1,6 +1,8 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
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.analytics.models.event.TransferAnalyticsEvent
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
@ -24,6 +26,7 @@ import org.junit.jupiter.api.Test
class UpdateTransferTransformerTest { class UpdateTransferTransformerTest {
private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true) private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true)
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
private val onActionDispatched: () -> Unit = mockk(relaxed = true) private val onActionDispatched: () -> Unit = mockk(relaxed = true)
@Test @Test
@ -112,6 +115,7 @@ class UpdateTransferTransformerTest {
// THEN // THEN
verifyOrder { verifyOrder {
analyticsEventHandler.send(ofType<TransferAnalyticsEvent.ButtonSend>())
onActionDispatched.invoke() onActionDispatched.invoke()
clickIntents.onSendClick(ScenarioUnavailabilityReason.None) clickIntents.onSendClick(ScenarioUnavailabilityReason.None)
} }
@ -130,6 +134,7 @@ class UpdateTransferTransformerTest {
// THEN // THEN
verifyOrder { verifyOrder {
analyticsEventHandler.send(ofType<TransferAnalyticsEvent.ButtonSell>())
onActionDispatched.invoke() onActionDispatched.invoke()
clickIntents.onSellClick(ScenarioUnavailabilityReason.None) clickIntents.onSellClick(ScenarioUnavailabilityReason.None)
} }
@ -225,6 +230,7 @@ class UpdateTransferTransformerTest {
// THEN // THEN
verifyOrder { verifyOrder {
analyticsEventHandler.send(ofType<TransferAnalyticsEvent.ButtonSwap>())
onActionDispatched.invoke() onActionDispatched.invoke()
clickIntents.onSwapFromClick(ScenarioUnavailabilityReason.None) clickIntents.onSwapFromClick(ScenarioUnavailabilityReason.None)
} }
@ -347,6 +353,7 @@ class UpdateTransferTransformerTest {
// Assert // Assert
verifyOrder { verifyOrder {
analyticsEventHandler.send(ofType<TransferAnalyticsEvent.ButtonSwapAndSend>())
onActionDispatched.invoke() onActionDispatched.invoke()
clickIntents.onSwapAndSendClick(ScenarioUnavailabilityReason.None) clickIntents.onSwapAndSendClick(ScenarioUnavailabilityReason.None)
} }
@ -405,6 +412,7 @@ class UpdateTransferTransformerTest {
// THEN // THEN
verify(exactly = 0) { onActionDispatched.invoke() } verify(exactly = 0) { onActionDispatched.invoke() }
verify(exactly = 0) { clickIntents.onSendClick(any()) } verify(exactly = 0) { clickIntents.onSendClick(any()) }
verify(exactly = 0) { analyticsEventHandler.send(any()) }
} }
private fun createTransformer( private fun createTransformer(
@ -414,6 +422,7 @@ class UpdateTransferTransformerTest {
actions = actions, actions = actions,
networkSource = networkSource, networkSource = networkSource,
clickIntents = clickIntents, clickIntents = clickIntents,
analyticsEventHandler = analyticsEventHandler,
onActionDispatched = onActionDispatched, onActionDispatched = onActionDispatched,
) )

View file

@ -125,6 +125,7 @@ internal class WalletClickIntents @Inject constructor(
} }
fun onTransferClick(userWalletId: UserWalletId) { fun onTransferClick(userWalletId: UserWalletId) {
analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonTransfer())
router.openTransfer(userWalletId) router.openTransfer(userWalletId)
} }