Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-07 16:06:04 +05:00
parent 2336cb6aaa
commit beceddf1fb
13 changed files with 89 additions and 0 deletions

View file

@ -442,6 +442,7 @@ internal class ChildFactory @Inject constructor(
selectedCurrency = route.selectedCurrency,
source = ChooseManagedTokensComponent.Source.valueOf(route.source.name),
showSendViaSwapNotification = route.showSendViaSwapNotification,
analyticsCategoryName = route.analyticsCategoryName,
),
componentFactory = chooseManagedTokensComponentFactory,
)

View file

@ -134,6 +134,7 @@ sealed class AppRoute(val path: String) : Route {
val selectedCurrency: CryptoCurrency?,
val source: Source,
val showSendViaSwapNotification: Boolean,
val analyticsCategoryName: String,
) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") {
enum class Source {
SendViaSwap,

View file

@ -234,5 +234,6 @@ sealed class AnalyticsParam {
const val RECEIVE_TOKEN = "Receive Token"
const val SEND_BLOCKCHAIN = "Send Blockchain"
const val RECEIVE_BLOCKCHAIN = "Receive Blockchain"
const val CHOSEN_TOKEN = "Token Chosen"
}
}

View file

@ -17,6 +17,7 @@ dependencies {
/* Project - Core */
implementation(projects.core.ui)
implementation(projects.core.decompose)
implementation(projects.core.analytics.models)
/* Compose */
implementation(deps.compose.runtime)

View file

@ -14,6 +14,7 @@ interface ChooseManagedTokensComponent : ComposableContentComponent {
val source: Source,
val showSendViaSwapNotification: Boolean,
val callback: ModelCallback? = null,
val analyticsCategoryName: String,
)
enum class Source {

View file

@ -0,0 +1,33 @@
package com.tangem.features.managetokens.component.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.CHOSEN_TOKEN
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
sealed class CommonManageTokensAnalyticEvents(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = category, event = event, params = params) {
data class TokenSearchClicked(
val categoryName: String,
) : CommonManageTokensAnalyticEvents(category = categoryName, event = "Token Search Clicked")
/** Searched token chosen event */
data class TokenSearched(
val categoryName: String,
val token: String?,
val blockchain: String?,
val isTokenChosen: Boolean,
) : CommonManageTokensAnalyticEvents(
category = categoryName,
event = "Token Searched",
params = buildMap {
put(CHOSEN_TOKEN, if (isTokenChosen) "Yes" else "No")
token?.let { put(TOKEN_PARAM, token) }
blockchain?.let { put(BLOCKCHAIN, blockchain) }
},
)
}

View file

@ -59,9 +59,11 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor(
context = childByContext(componentContext),
params = SwapChooseTokenNetworkComponent.Params(
userWalletId = config.userWalletId,
analyticsCategoryName = params.analyticsCategoryName,
initialCurrency = config.initialCurrency,
selectedCurrency = config.selectedCurrency,
token = config.token,
isSearchedToken = config.isSearchedToken,
onDismiss = model.bottomSheetNavigation::dismiss,
onResult = { swapCurrencies, cryptoCurrency ->
componentScope.launch {

View file

@ -14,5 +14,6 @@ internal sealed class ChooseManageTokensBottomSheetConfig {
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val token: ManagedCryptoCurrency.Token,
val isSearchedToken: Boolean,
) : ChooseManageTokensBottomSheetConfig()
}

View file

@ -5,6 +5,7 @@ import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.ui.notifications.NotificationId
import com.tangem.common.ui.notifications.NotificationUM
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
@ -22,6 +23,7 @@ import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent.Source
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM
import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM
@ -40,12 +42,14 @@ import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.isNotEmpty
@Suppress("LongParameterList")
@ModelScoped
internal class ChooseManagedTokensModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val uiMessageSender: UiMessageSender,
private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
) : Model() {
@ -60,6 +64,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
initialCurrency = params.initialCurrency,
selectedCurrency = params.selectedCurrency,
token = token,
isSearchedToken = uiState.value.readContent.search.isActive,
),
)
},
@ -132,6 +137,17 @@ internal class ChooseManagedTokensModel @Inject constructor(
private fun observeSearchQueryChanges() {
uiState
.distinctUntilChanged { old, new ->
if (!new.readContent.search.isActive && old.readContent.search.isActive) {
analyticsEventHandler.send(
CommonManageTokensAnalyticEvents.TokenSearched(
params.analyticsCategoryName,
token = null,
blockchain = null,
isTokenChosen = false,
),
)
}
// It's also used to skip search activation to avoid searching an empty query
old.readContent.search.query == new.readContent.search.query && new.readContent.search.isActive
}
@ -251,6 +267,11 @@ internal class ChooseManagedTokensModel @Inject constructor(
}
private fun toggleSearchBar(isActive: Boolean) {
if (isActive) {
analyticsEventHandler.send(
CommonManageTokensAnalyticEvents.TokenSearchClicked(params.analyticsCategoryName),
)
}
uiState.update { state ->
@StringRes val placeholderTextRes = if (isActive) {
R.string.manage_tokens_search_placeholder

View file

@ -24,6 +24,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendEntryPointComponent
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel
import com.tangem.features.swap.v2.api.SendWithSwapComponent
import dagger.assisted.Assisted
@ -126,6 +127,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor(
selectedCurrency = null,
showSendViaSwapNotification = showSendViaSwapNotification,
callback = model,
analyticsCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY,
),
)
}

View file

@ -12,8 +12,10 @@ interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent {
data class Params(
val userWalletId: UserWalletId,
val initialCurrency: CryptoCurrency,
val analyticsCategoryName: String,
val selectedCurrency: CryptoCurrency?,
val token: ManagedCryptoCurrency.Token,
val isSearchedToken: Boolean,
val onDismiss: () -> Unit,
val onResult: (SwapCurrencies, CryptoCurrency) -> Unit,
)

View file

@ -270,6 +270,7 @@ internal class SwapAmountModel @Inject constructor(
selectedCurrency = selectedCurrency.takeIf { isEditMode },
source = AppRoute.ChooseManagedTokens.Source.SendViaSwap,
showSendViaSwapNotification = showSendViaSwapNotification,
analyticsCategoryName = params.analyticsCategoryName,
),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model
import arrow.core.getOrElse
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
@ -10,6 +11,8 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.swap.models.SwapCurrencies
import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM
import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM
@ -26,6 +29,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class SwapChooseTokenNetworkModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
@ -34,6 +38,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val swapChooseTokenAlertFactory: SwapChooseTokenAlertFactory,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params: SwapChooseTokenNetworkComponent.Params = paramsContainer.require()
@ -103,6 +108,23 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
private fun onSwapTokenClick(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency) {
val prevSelectedCurrency = params.selectedCurrency?.network
analyticsEventHandler.send(
CommonSendAnalyticEvents.TokenChosen(
categoryName = params.analyticsCategoryName,
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
if (params.isSearchedToken) {
analyticsEventHandler.send(
CommonManageTokensAnalyticEvents.TokenSearched(
categoryName = params.analyticsCategoryName,
token = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
isTokenChosen = true,
),
)
}
if (prevSelectedCurrency == null || cryptoCurrency.network == prevSelectedCurrency) {
params.onResult(swapCurrencies, cryptoCurrency)
} else {