Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-16 16:25:39 +04:00
parent e0a845a474
commit 9bcf93922c
12 changed files with 213 additions and 121 deletions

View file

@ -9,9 +9,13 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
import com.tangem.domain.managetokens.FindTokenUseCase
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
@ -41,6 +45,10 @@ internal class CustomTokenFormModel @Inject constructor(
private val messageSender: UiMessageSender,
private val customTokenFormManager: CustomCurrencyFormBuilder,
private val analyticsEventHandler: AnalyticsEventHandler,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
private val findTokenUseCase: FindTokenUseCase,
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
paramsContainer: ParamsContainer,
customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory,
) : Model() {
@ -48,7 +56,13 @@ internal class CustomTokenFormModel @Inject constructor(
private val params: CustomTokenFormComponent.Params = paramsContainer.require()
private var createdCurrency: CryptoCurrency? = null
private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode)
private val customCurrencyValidator = CustomCurrencyValidator(useCasesFacade)
private val customCurrencyValidator = CustomCurrencyValidator(
userWalletId = params.mode.userWalletId,
useCasesFacade = useCasesFacade,
createCryptoCurrencyUseCase = createCryptoCurrencyUseCase,
findTokenUseCase = findTokenUseCase,
validateTokenFormUseCase = validateTokenFormUseCase,
)
val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
value = getInitialState(),
@ -164,8 +178,9 @@ internal class CustomTokenFormModel @Inject constructor(
isAlreadyAdded: Boolean,
isCustom: Boolean,
) = modelScope.launch {
val needColdWalletInteraction = useCasesFacade.needColdWalletInteraction(
network = mapOf(currency.network.backendId to getDerivationPath().value),
val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = params.mode.userWalletId,
networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value),
)
state.update { state ->

View file

@ -1,11 +1,15 @@
package com.tangem.features.managetokens.utils
import arrow.core.getOrElse
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
import com.tangem.domain.managetokens.FindTokenUseCase
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.AddCustomTokenForm
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.utils.list.CustomTokenFormUseCasesFacade
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveInAndJoin
@ -15,7 +19,11 @@ import kotlinx.coroutines.launch
import timber.log.Timber
internal class CustomCurrencyValidator(
private val userWalletId: UserWalletId,
private val useCasesFacade: CustomTokenFormUseCasesFacade,
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
private val findTokenUseCase: FindTokenUseCase,
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
) {
private val validateFormJobHolder = JobHolder()
@ -41,10 +49,7 @@ internal class CustomCurrencyValidator(
) = coroutineScope {
updateStatus(Status.Validating)
val result = useCasesFacade.validateTokenFormUseCase(
networkId = networkId,
formValues = formValues,
)
val result = validateTokenFormUseCase(networkId = networkId, formValues = formValues)
val validatedForm = result.getOrElse { e ->
updateStatus(Status.FormValidationException(e))
@ -90,7 +95,8 @@ internal class CustomCurrencyValidator(
updateStatus(Status.SearchingToken)
val foundToken = useCasesFacade.findTokenUseCase(
val foundToken = findTokenUseCase.invoke(
userWalletId = userWalletId,
contractAddress = validatedForm.contractAddress,
networkId = networkId,
derivationPath = derivationPath,
@ -121,7 +127,8 @@ internal class CustomCurrencyValidator(
) {
updateStatus(Status.SearchingToken)
val token = useCasesFacade.findTokenUseCase(
val token = findTokenUseCase.invoke(
userWalletId = userWalletId,
contractAddress = validatedForm.contractAddress,
networkId = networkId,
derivationPath = derivationPath,
@ -148,7 +155,8 @@ internal class CustomCurrencyValidator(
derivationPath: Network.DerivationPath,
validatedForm: AddCustomTokenForm.Validated.All?,
) {
val currency = useCasesFacade.createCryptoCurrencyUseCase(
val currency = createCryptoCurrencyUseCase.invoke(
userWalletId = userWalletId,
networkId = networkId,
derivationPath = derivationPath,
formValues = validatedForm,

View file

@ -1,47 +1,30 @@
package com.tangem.features.managetokens.utils.list
import arrow.core.Either
import arrow.core.NonEmptyList
import arrow.core.left
import arrow.core.right
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
import com.tangem.domain.managetokens.FindTokenUseCase
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
import com.tangem.domain.managetokens.model.AddCustomTokenForm
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.features.managetokens.component.AddCustomTokenMode
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("LongParameterList")
internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
private val findTokenUseCase: FindTokenUseCase,
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
private val singleAccountSupplier: SingleAccountSupplier,
@Assisted private val mode: AddCustomTokenMode,
) {
suspend fun needColdWalletInteraction(network: Map<String, String?>): Boolean = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = mode.userWalletId,
networksWithDerivationPath = network,
)
}
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> = when (mode) {
is AddCustomTokenMode.Account -> {
saveCryptoCurrenciesUseCase(accountId = mode.accountId, add = currency)
@ -67,7 +50,19 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
derivationPath: Network.DerivationPath,
contractAddress: String?,
): Either<Throwable, Boolean> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Account -> {
val account = singleAccountSupplier.getSyncOrNull(
params = SingleAccountProducer.Params(accountId = mode.accountId),
)
?: return IllegalStateException("Account not found").left()
account.cryptoCurrencies.none { currency ->
networkId == currency.network.id &&
derivationPath == currency.network.derivationPath &&
contractAddress.equals(currency.id.contractAddress, ignoreCase = true)
}
.right()
}
is AddCustomTokenMode.Wallet -> checkIsCurrencyNotAddedUseCase.invoke(
userWalletId = mode.userWalletId,
networkId = networkId,
@ -76,45 +71,6 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
)
}
suspend fun createCryptoCurrencyUseCase(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
formValues: AddCustomTokenForm.Validated.All?,
): Either<Throwable, CryptoCurrency> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> createCryptoCurrencyUseCase.invoke(
userWalletId = mode.userWalletId,
networkId = networkId,
derivationPath = derivationPath,
formValues = formValues,
)
}
suspend fun findTokenUseCase(
contractAddress: String,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): Either<FindTokenException, CryptoCurrency.Token> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> findTokenUseCase.invoke(
userWalletId = mode.userWalletId,
contractAddress = contractAddress,
networkId = networkId,
derivationPath = derivationPath,
)
}
suspend fun validateTokenFormUseCase(
networkId: Network.ID,
formValues: AddCustomTokenForm.Raw,
): Either<NonEmptyList<CustomTokenFormValidationException>, AddCustomTokenForm.Validated> = when (mode) {
is AddCustomTokenMode.Account -> TODO("Account")
is AddCustomTokenMode.Wallet -> validateTokenFormUseCase.invoke(
networkId = networkId,
formValues = formValues,
)
}
@AssistedFactory
interface Factory {
fun create(mode: AddCustomTokenMode): CustomTokenFormUseCasesFacade

View file

@ -90,8 +90,9 @@ internal class ManageTokensListManager @AssistedInject constructor(
*/
suspend fun launchPagination(isCollapsed: Boolean) = coroutineScope {
val loadUserTokensFromRemote = when (mode) {
is ManageTokensMode.Wallet -> source == ManageTokensSource.ONBOARDING
is ManageTokensMode.Wallet,
is ManageTokensMode.Account,
-> source == ManageTokensSource.ONBOARDING
ManageTokensMode.None,
-> false
}

View file

@ -2,7 +2,11 @@ package com.tangem.features.managetokens.utils.list
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManageTokensListConfig
@ -28,6 +32,8 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
private val customTokensRepository: CustomTokensRepository,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val singleAccountSupplier: SingleAccountSupplier,
@Assisted private val mode: ManageTokensMode,
) {
@ -35,12 +41,21 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
get() = IllegalStateException("Unsupported")
fun manageTokensListConfig(searchText: String?): ManageTokensListConfig {
val userWalletId: UserWalletId? = when (mode) {
is ManageTokensMode.Account -> TODO("Account")
ManageTokensMode.None -> null
is ManageTokensMode.Wallet -> mode.userWalletId
return when (mode) {
is ManageTokensMode.Account -> {
ManageTokensListConfig.Account(accountId = mode.accountId, searchText = searchText)
}
is ManageTokensMode.Wallet -> {
ManageTokensListConfig.Wallet(userWalletId = mode.userWalletId, searchText = searchText)
}
ManageTokensMode.None -> {
if (accountsFeatureToggles.isFeatureEnabled) {
ManageTokensListConfig.Account(accountId = null, searchText = searchText)
} else {
ManageTokensListConfig.Wallet(userWalletId = null, searchText = searchText)
}
}
}
return ManageTokensListConfig(userWalletId, searchText)
}
suspend fun removeCustomCurrencyUseCase(customCurrency: ManagedCryptoCurrency.Custom): Either<Throwable, Unit> {
@ -67,7 +82,21 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
tempRemovedTokens: Map<ManagedCryptoCurrency.Token, Set<Network>>,
): Either<Throwable, Boolean> {
return when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Account -> {
val added = tempAddedTokens.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId)
val removed = tempRemovedTokens.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId)
val account = singleAccountSupplier.getSyncOrNull(
params = SingleAccountProducer.Params(accountId = mode.accountId),
)
?: return IllegalStateException("Account not found").left()
(account.cryptoCurrencies + added - removed).any {
it is CryptoCurrency.Token && it.network.backendId == network.backendId &&
it.network.derivationPath == network.derivationPath
}
.right()
}
is ManageTokensMode.Wallet -> checkHasLinkedTokensUseCase.invoke(
userWalletId = mode.userWalletId,
network = network,
@ -82,7 +111,10 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
sourceNetwork: ManagedCryptoCurrency.SourceNetwork,
): Either<Throwable, CurrencyUnsupportedState?> {
return when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Account -> checkCurrencyUnsupportedUseCase.invoke(
userWalletId = mode.accountId.userWalletId,
sourceNetwork = sourceNetwork,
)
is ManageTokensMode.Wallet -> checkCurrencyUnsupportedUseCase.invoke(
userWalletId = mode.userWalletId,
sourceNetwork = sourceNetwork,
@ -92,7 +124,10 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
}
suspend fun needColdWalletInteraction(network: Map<String, Nothing?>): Boolean = when (mode) {
is ManageTokensMode.Account -> TODO("Account")
is ManageTokensMode.Account -> coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = mode.accountId.userWalletId,
networksWithDerivationPath = network,
)
is ManageTokensMode.Wallet -> coldWalletAndHasMissedDerivationsUseCase.invoke(
userWalletId = mode.userWalletId,
networksWithDerivationPath = network,