Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-03 11:21:00 +04:00
parent dff5b1f27c
commit 978d7c1da4
20 changed files with 202 additions and 855 deletions

View file

@ -5,7 +5,6 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationUM
@ -16,29 +15,23 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
@ -55,7 +48,6 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.CommonSendRoute.*
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
@ -74,7 +66,6 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.properties.Delegates
import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned
internal interface SendComponentCallback :
SendAmountComponent.ModelCallback,
@ -90,7 +81,6 @@ internal class SendModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
@ -108,7 +98,6 @@ internal class SendModel @Inject constructor(
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
private val analyticsEventHandler: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model(), SendComponentCallback {
private val params: SendComponent.Params = paramsContainer.require()
@ -254,7 +243,7 @@ internal class SendModel @Inject constructor(
showAlertError()
}
suspend fun prepareTransferTransaction(): Either<Throwable, TransactionData> {
private suspend fun prepareTransferTransaction(): Either<Throwable, TransactionData> {
val predefinedValues = predefinedValues
val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value
return if (predefinedValues is PredefinedValues.Content.Deeplink) {
@ -360,34 +349,24 @@ internal class SendModel @Inject constructor(
ifRight = { wallet ->
userWallet = wallet
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase(
getAccountCurrencyStatusUseCase(
userWalletId = params.userWalletId,
currency = cryptoCurrency,
).onEach { (account, cryptoCurrencyStatus) ->
isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync()
accountFlow.value = account
cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus
feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = params.userWalletId,
currency = cryptoCurrency,
).onEach { (account, cryptoCurrencyStatus) ->
isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync()
accountFlow.value = account
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull() ?: cryptoCurrencyStatus
cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus
feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull() ?: cryptoCurrencyStatus
if (params.amount != null) {
router.replaceAll(Confirm)
}
}.flowOn(dispatchers.default)
.launchIn(modelScope)
} else {
val isSingleWalletWithToken = wallet is UserWallet.Cold &&
wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
val isMultiCurrency = wallet.isMultiCurrency
getCurrenciesStatusUpdates(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
)
}
if (params.amount != null) {
router.replaceAll(Confirm)
}
}.flowOn(dispatchers.default)
.launchIn(modelScope)
},
ifLeft = { error ->
Timber.w(error.toString())
@ -409,77 +388,6 @@ internal class SendModel @Inject constructor(
.saveIn(balanceHidingJobHolder)
}
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) {
getCurrencyStatus(
isSingleWalletWithToken = isSingleWalletWithToken,
isMultiCurrency = isMultiCurrency,
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoCurrencyStatus ->
cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus
feeCryptoCurrencyStatusFlow.value = getFeeCurrencyStatus(cryptoCurrencyStatus, isMultiCurrency)
if (params.amount != null) {
router.replaceAll(CommonSendRoute.Confirm)
}
},
ifLeft = {
sendConfirmAlertFactory.getGenericErrorState(
onFailedTxEmailClick = {
onFailedTxEmailClick(it.toString())
},
popBack = router::pop,
)
},
)
}.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun getCurrencyStatus(
isSingleWalletWithToken: Boolean,
isMultiCurrency: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return when {
isSingleWalletWithToken -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = params.userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = true,
)
isMultiCurrency -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = params.userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = false,
)
else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = params.userWalletId)
}
}
fun getSelectedFeeToken(): CryptoCurrency {
val feeUMV2 = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
return if (isFeeInTokenCurrency) {
feeUMV2.feeExtraInfo.feeCryptoCurrencyStatus.currency
} else {
feeCryptoCurrencyStatusFlow.value.currency
}
}
private suspend fun getFeeCurrencyStatus(
cryptoCurrencyStatus: CryptoCurrencyStatus,
isMultiCurrency: Boolean,
): CryptoCurrencyStatus {
return if (isMultiCurrency) {
getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = params.userWalletId,
cryptoCurrencyStatus = cryptoCurrencyStatus,
).getOrNull() ?: cryptoCurrencyStatus
} else {
cryptoCurrencyStatus
}
}
private fun subscribeOnQRScannerResult() {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }

View file

@ -11,12 +11,10 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
@ -27,7 +25,6 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.transaction.error.GetFeeError
@ -66,7 +63,6 @@ internal class NFTSendModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
@ -77,7 +73,6 @@ internal class NFTSendModel @Inject constructor(
private val nftSendSuccessTrigger: NFTSendSuccessTrigger,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback {
val params: NFTSendComponent.Params = paramsContainer.require()
@ -186,31 +181,24 @@ internal class NFTSendModel @Inject constructor(
?.firstOrNull { it is CryptoCurrency.Coin && it.network == nftAsset.network }
?: return@launch
if (accountsFeatureToggles.isFeatureEnabled) {
getAccountCurrencyStatusUseCase(
userWalletId,
cryptoCurrency,
).onEach { (maybeAccount, cryptoStatus) ->
account = maybeAccount
isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
getAccountCurrencyStatusUseCase(
userWalletId,
cryptoCurrency,
).onEach { (maybeAccount, cryptoStatus) ->
account = maybeAccount
isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
cryptoCurrencyStatus = cryptoStatus
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoStatus,
).getOrNull() ?: cryptoStatus
cryptoCurrencyStatus = cryptoStatus
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoStatus,
).getOrNull() ?: cryptoStatus
if (uiState.value.destinationUM is DestinationUM.Empty) {
router.replaceAll(Destination(isEditMode = false))
}
}.flowOn(dispatchers.default)
.launchIn(modelScope)
} else {
getCurrenciesStatusUpdates(
isSingleWalletWithToken = wallet is UserWallet.Cold &&
wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
}
if (uiState.value.destinationUM is DestinationUM.Empty) {
router.replaceAll(Destination(isEditMode = false))
}
}.flowOn(dispatchers.default)
.launchIn(modelScope)
},
ifLeft = {
alertFactory.getGenericErrorState(::onFailedTxEmailClick)
@ -246,34 +234,6 @@ internal class NFTSendModel @Inject constructor(
}
}
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) {
getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = isSingleWalletWithToken,
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoStatus ->
cryptoCurrencyStatus = cryptoStatus
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoStatus,
).getOrNull() ?: cryptoStatus
if (uiState.value.destinationUM is DestinationUM.Empty) {
router.replaceAll(Destination(isEditMode = false))
}
},
ifLeft = {
alertFactory.getGenericErrorState(
onFailedTxEmailClick = { onFailedTxEmailClick(it.toString()) },
popBack = { router.pop() },
)
},
)
}.launchIn(modelScope)
}
private fun initialState(): NFTSendUM = NFTSendUM(
destinationUM = DestinationUM.Empty(),
feeSelectorUM = FeeSelectorUM.Loading,

View file

@ -11,18 +11,14 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.network.CryptoCurrencyAddress
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
import com.tangem.domain.transaction.usecase.IsSelfSendAvailableUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
@ -63,7 +59,6 @@ internal class SendDestinationModel @Inject constructor(
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase,
@ -71,7 +66,6 @@ internal class SendDestinationModel @Inject constructor(
private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
) : Model(), SendDestinationClickIntents {
private val params: SendDestinationComponentParams = paramsContainer.require()
@ -191,15 +185,7 @@ internal class SendDestinationModel @Inject constructor(
private fun getWalletsAndRecent() {
combine(
flow = if (accountsFeatureToggles.isFeatureEnabled) {
getAddedAddresses()
} else {
getWalletsUseCase().conflate().map {
waitForDelay(RECENT_LOAD_DELAY) {
it.toAvailableWallets()
}
}
},
flow = getAddedAddresses(),
flow2 = getFixedTxHistoryItemsUseCase(
userWalletId = userWalletId,
currency = cryptoCurrency,
@ -226,48 +212,6 @@ internal class SendDestinationModel @Inject constructor(
}.flowOn(dispatchers.default).launchIn(modelScope)
}
private suspend fun List<UserWallet>.toAvailableWallets(): List<DestinationWalletUM> {
return coroutineScope {
val cryptoCurrencyNetwork = cryptoCurrency.network
return@coroutineScope filterNot { it.isLocked }
.map { wallet ->
async {
val addresses = if (!wallet.isMultiCurrency) {
getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { cryptoCurrency ->
if (cryptoCurrency.network.rawId == cryptoCurrencyNetwork.rawId) {
getNetworkAddressesUseCase.invokeSync(
userWalletId = wallet.walletId,
networkRawId = cryptoCurrency.network.id.rawId,
)
} else {
null
}
}
} else {
getNetworkAddressesUseCase.invokeSync(
userWalletId = wallet.walletId,
networkRawId = cryptoCurrencyNetwork.id.rawId,
)
}
wallet to addresses
}
}.awaitAll()
.asSequence()
.mapNotNull { (wallet, addresses) ->
addresses?.map { (cryptoCurrency, address) ->
DestinationWalletUM(
name = wallet.name,
address = address,
cryptoCurrency = cryptoCurrency,
userWalletId = wallet.walletId,
)
}
}.flatten()
.toList()
}
}
private fun getAddedAddresses(): Flow<List<DestinationWalletUM>> {
return combine(
flow = getWalletsUseCase().conflate(),