Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-09 18:25:02 +07:00
parent 4dc994e8cf
commit 03da1731ea
24 changed files with 335 additions and 92 deletions

View file

@ -25,6 +25,8 @@ dependencies {
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.account.status)
implementation(projects.domain.account)
implementation(projects.domain.card)
implementation(projects.domain.legacy)
implementation(projects.domain.manageTokens)
@ -35,6 +37,14 @@ dependencies {
implementation(projects.domain.swap.models)
implementation(projects.domain.notifications)
// region Project - Libs
implementation(projects.libs.crypto)
// endregion
// region Tangem SDKs
implementation(tangemDeps.blockchain)
// endregion
/* AndroidX */
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)

View file

@ -54,6 +54,7 @@ internal sealed class ManageTokensUM {
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
scrollToTop: StateEvent<Unit> = this.scrollToTop,
needToInteractWithColdWallet: Boolean = this is ManageContent && this.needToInteractWithColdWallet,
topBar: ManageTokensTopBarUM? = this.topBar,
): ManageTokensUM {
return when (this) {
is ManageContent -> copy(
@ -65,6 +66,7 @@ internal sealed class ManageTokensUM {
isSavingInProgress = isSavingInProgress,
scrollToTop = scrollToTop,
needToInteractWithColdWallet = needToInteractWithColdWallet,
topBar = topBar,
)
is ReadContent -> copy(
search = search,
@ -72,6 +74,7 @@ internal sealed class ManageTokensUM {
isInitialBatchLoading = isInitialBatchLoading,
isNextBatchLoading = isNextBatchLoading,
scrollToTop = scrollToTop,
topBar = topBar,
)
}
}

View file

@ -3,13 +3,22 @@ package com.tangem.features.managetokens.model
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.ui.account.toUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.network.Network
import com.tangem.features.managetokens.component.AddCustomTokenMode
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
@ -25,10 +34,12 @@ import com.tangem.features.managetokens.entity.item.SelectableItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel
import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -38,6 +49,8 @@ internal class CustomTokenSelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase,
private val messageSender: UiMessageSender,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val accountsFeatureToggles: AccountsFeatureToggles,
paramsContainer: ParamsContainer,
) : Model() {
@ -146,9 +159,8 @@ internal class CustomTokenSelectorModel @Inject constructor(
return derivationPaths
}
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> {
return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error))
messageSender.send(message)
@ -168,7 +180,60 @@ internal class CustomTokenSelectorModel @Inject constructor(
fun selectCustomDerivationPath(value: SelectedDerivationPath) {
when (params) {
is NetworkSelector -> return
is DerivationPathSelector -> params.onDerivationPathSelected(value)
is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) {
params.checkAccountDerivation(value)
} else {
params.onDerivationPathSelected(value)
}
}
}
private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) =
modelScope.launch {
val accountName = derivationPath.id
?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer)
?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } }
?.let { accountNode ->
fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount &&
this.account.derivationIndex.value.toLong() == accountNode
val accounts = singleAccountStatusListSupplier(mode.userWalletId)
.first().accountStatuses
val account = accounts.find {
when (it) {
is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain()
}
}
val accountName = when (account) {
is AccountStatus.CryptoPortfolio -> account.account.accountName.toUM()
null -> null
}
accountName
}
if (accountName == null) {
onDerivationPathSelected(derivationPath)
} else {
showAccountNameExist(
accountName = accountName.value,
onClick = { onDerivationPathSelected(derivationPath) },
)
}
}
private fun showAccountNameExist(accountName: TextReference, onClick: () -> Unit) {
val firstAction = EventMessageAction(
title = resourceReference(R.string.common_got_it),
onClick = onClick,
)
val dialogMessage = DialogMessage(
title = resourceReference(R.string.custom_token_another_account_dialog_title),
message = resourceReference(
R.string.custom_token_another_account_dialog_description,
wrappedList(accountName),
),
firstActionBuilder = { firstAction },
)
messageSender.send(dialogMessage)
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensComponent
@ -40,13 +41,14 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@ModelScoped
internal class ManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val messageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
paramsContainer: ParamsContainer,
@ -86,6 +88,7 @@ internal class ManageTokensModel @Inject constructor(
modelScope.launch {
manageTokensListManager.launchPagination(isCollapsed = true)
}
checkIsSupportAddCustomTokens()
}
fun reloadList() {
@ -105,16 +108,34 @@ internal class ManageTokensModel @Inject constructor(
}
}
private fun getTopBarInitialState(): ManageTokensTopBarUM = when (params.mode) {
is ManageTokensMode.Wallet -> manageContentTopBar()
is ManageTokensMode.Account -> ManageTokensTopBarUM.ReadContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
)
ManageTokensMode.None -> ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
)
}
private fun manageContentTopBar() = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_plus_24,
onClicked = ::navigateToAddCustomToken,
),
)
private fun createReadContentModel(): ManageTokensUM.ReadContent {
return ManageTokensUM.ReadContent(
popBack = router::pop,
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getLoadingItems(),
topBar = ManageTokensTopBarUM.ReadContent(
title = resourceReference(R.string.common_search_tokens),
onBackButtonClick = router::pop,
),
topBar = getTopBarInitialState(),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
@ -132,14 +153,7 @@ internal class ManageTokensModel @Inject constructor(
isInitialBatchLoading = true,
isNextBatchLoading = false,
items = getLoadingItems(),
topBar = ManageTokensTopBarUM.ManageContent(
title = resourceReference(id = R.string.main_manage_tokens),
onBackButtonClick = router::pop,
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_plus_24,
onClicked = ::navigateToAddCustomToken,
),
),
topBar = getTopBarInitialState(),
search = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
@ -174,6 +188,20 @@ internal class ManageTokensModel @Inject constructor(
.launchIn(modelScope)
}
private fun checkIsSupportAddCustomTokens() {
when (val mode = params.mode) {
is ManageTokensMode.Account -> modelScope.launch {
val mainAccount = singleAccountStatusListSupplier(mode.accountId.userWalletId).first().mainAccount
if (mode.accountId == mainAccount.account.accountId) {
state.update { it.copySealed(topBar = manageContentTopBar()) }
}
}
ManageTokensMode.None,
is ManageTokensMode.Wallet,
-> Unit // use init state
}
}
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
val updatedState = state.updateAndGet { state ->
state.copySealed(

View file

@ -57,6 +57,7 @@ import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
@ -442,20 +443,23 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
showTangemIcon = true,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("0x"),
mode = ManageTokensMode.Wallet(UserWalletId("0x")),
),
),
PreviewManageTokensComponent(
isLoading = false,
showTangemIcon = true,
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
mode = ManageTokensMode.None,
),
),
PreviewManageTokensComponent(
isLoading = false,
showTangemIcon = false,
params = ManageTokensComponent.Params(
source = ManageTokensSource.ONBOARDING,
userWalletId = UserWalletId("0x"),
mode = ManageTokensMode.Wallet(UserWalletId("0x")),
),
),
)