Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-03 15:03:36 +05:00
parent 8b0f0e589e
commit f94370c41c
7 changed files with 139 additions and 62 deletions

View file

@ -1,11 +1,13 @@
package com.tangem.features.send.impl.presentation.state.recipient
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.transaction.error.ValidateAddressError
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.SendUiState
@ -18,10 +20,14 @@ internal class RecipientSendFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val isUtxoConsolidationAvailableProvider: Provider<Boolean>,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
) {
private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientWalletListConverter()
SendRecipientWalletListConverter(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
isUtxoConsolidationAvailableProvider = isUtxoConsolidationAvailableProvider,
)
}
private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientHistoryListConverter(
@ -60,7 +66,10 @@ internal class RecipientSendFactory(
)
}
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
fun getOnRecipientAddressValidState(
value: String,
maybeValidAddress: Either<ValidateAddressError, Unit>,
): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
@ -73,21 +82,28 @@ internal class RecipientSendFactory(
Timber.e("Failed to validateWalletMemoUseCase: $it")
false
}
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isPrimaryButtonEnabled = isValidMemo && maybeValidAddress.isRight(),
isValidating = false,
addressTextField = recipientState.addressTextField.copy(
error = when {
!isValidAddress -> resourceReference(R.string.send_recipient_address_error)
isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet)
else -> null
},
isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet,
error = maybeValidAddress.fold(
ifLeft = {
when (it) {
ValidateAddressError.InvalidAddress -> resourceReference(
R.string.send_recipient_address_error,
)
ValidateAddressError.AddressInWallet -> resourceReference(
R.string.send_error_address_same_as_wallet,
)
else -> null
}
},
ifRight = { null },
),
isError = value.isNotEmpty() && maybeValidAddress.isLeft(),
),
),
)
@ -131,13 +147,11 @@ internal class RecipientSendFactory(
Timber.e("Failed to validateWalletMemoUseCase: $it")
false
}
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet,
isPrimaryButtonEnabled = isValidMemo && isValidAddress,
isValidating = false,
memoTextField = recipientState.memoTextField?.copy(
isError = value.isNotEmpty() && !isValidMemo,
@ -162,11 +176,10 @@ internal class RecipientSendFactory(
)
}
fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState {
fun getHiddenRecentListState(isNotValid: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isNotValid = isAddressInWallet || !isValidAddress
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(

View file

@ -1,16 +1,22 @@
package com.tangem.features.send.impl.presentation.state.recipient
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_DEFAULT_COUNT
import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_KEY_TAG
import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
internal class SendRecipientWalletListConverter :
internal class SendRecipientWalletListConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val isUtxoConsolidationAvailableProvider: Provider<Boolean>,
) :
Converter<List<AvailableWallet?>, PersistentList<SendRecipientListContent>> {
override fun convert(value: List<AvailableWallet?>): PersistentList<SendRecipientListContent> {
return value.filterWallets().ifEmpty {
@ -20,8 +26,18 @@ internal class SendRecipientWalletListConverter :
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
var walletsCounter = 0
val currentAddress: String = runCatching {
cryptoCurrencyStatusProvider().value.networkAddress?.defaultAddress?.value
}.getOrNull().orEmpty()
return this.filterNotNull()
.filter { it.address.isNotBlank() }
.filter {
val isCoin = it.cryptoCurrency is CryptoCurrency.Coin
val isNotSameAddress = it.address != currentAddress
val isNotBlankAddress = it.address.isNotBlank()
isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailableProvider())
}
.groupBy { item -> item.name }
.values.map { wallets ->
val groupedByWallet = wallets.groupBy { it.userWalletId }

View file

@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -34,10 +35,11 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.transaction.error.ValidateAddressError
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
@ -93,6 +95,7 @@ internal class SendViewModel @Inject constructor(
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
validateTransactionUseCase: ValidateTransactionUseCase,
currencyChecksRepository: CurrencyChecksRepository,
@ -135,6 +138,7 @@ internal class SendViewModel @Inject constructor(
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable },
validateWalletMemoUseCase = validateWalletMemoUseCase,
)
@ -198,6 +202,7 @@ internal class SendViewModel @Inject constructor(
private var userWallet: UserWallet by Delegates.notNull()
private var userWallets: List<AvailableWallet> = emptyList()
private var isAmountSubtractAvailable: Boolean = false
private var isUtxoConsolidationAvailable: Boolean = false
private var isTapHelpPreviewEnabled: Boolean = false
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
@ -248,6 +253,7 @@ internal class SendViewModel @Inject constructor(
ifRight = { wallet ->
userWallet = wallet
checkIfSubtractAvailable()
checkIfUtxoConsolidationAvailable()
val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
val isMultiCurrency = wallet.isMultiCurrency
@ -424,9 +430,6 @@ internal class SendViewModel @Inject constructor(
}
private suspend fun List<UserWallet>.toAvailableWallets(): List<AvailableWallet> {
val currentAddress: String = kotlin.runCatching {
cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
}.getOrNull().orEmpty()
return filterNot { it.isLocked }
.mapNotNull { wallet ->
val addresses = if (!wallet.isMultiCurrency) {
@ -440,16 +443,14 @@ internal class SendViewModel @Inject constructor(
} else {
getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrency.network)
}
addresses
?.filter { it.address != currentAddress && it.cryptoCurrency is CryptoCurrency.Coin }
?.map { (cryptoCurrency, address) ->
AvailableWallet(
name = wallet.name,
address = address,
cryptoCurrency = cryptoCurrency,
userWalletId = wallet.walletId,
)
}
addresses?.map { (cryptoCurrency, address) ->
AvailableWallet(
name = wallet.name,
address = address,
cryptoCurrency = cryptoCurrency,
userWalletId = wallet.walletId,
)
}
}.flatten()
}
@ -624,8 +625,15 @@ internal class SendViewModel @Inject constructor(
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(value)
uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) }
autoNextFromRecipient(type, isValidAddress)
type?.let {
analyticsEventHandler.send(
SendAnalyticEvents.AddressEntered(
it,
isValidAddress.isRight(),
),
)
}
autoNextFromRecipient(type, isValidAddress.isRight())
}
}.saveIn(addressValidationJobHolder)
}
@ -636,23 +644,22 @@ internal class SendViewModel @Inject constructor(
uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
val recipientState = uiState.getRecipientState(stateRouter.isEditState)
val isValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty())
uiState = recipientStateFactory.getOnRecipientMemoValidState(value, isValidAddress)
val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty())
uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight())
}
}.saveIn(memoValidationJobHolder)
}
private suspend fun validateAddress(value: String): Boolean = runCatching {
val isValidAddress = validateWalletAddressUseCase(
private suspend fun validateAddress(value: String): Either<ValidateAddressError, Unit> = runCatching {
val maybeValidAddress = validateWalletAddressUseCase(
userWalletId = userWalletId,
network = cryptoCurrency.network,
address = value,
).getOrElse { false }
val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses
?.any { it.value == value } ?: true
onEnteredValidAddress(isValidAddress, isAddressInWallet)
return isValidAddress
}.getOrElse { false }
currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses,
)
onEnteredValidAddress(maybeValidAddress.isLeft())
maybeValidAddress
}.getOrElse { ValidateAddressError.DataError(it).left() }
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
@ -665,11 +672,8 @@ internal class SendViewModel @Inject constructor(
} ?: false
}
private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) {
uiState = recipientStateFactory.getHiddenRecentListState(
isAddressInWallet = isAddressInWallet,
isValidAddress = isValidAddress,
)
private fun onEnteredValidAddress(isNotValid: Boolean) {
uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid)
}
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
@ -739,6 +743,13 @@ internal class SendViewModel @Inject constructor(
)
}
private suspend fun checkIfUtxoConsolidationAvailable() {
isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
}
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation
val amountState = uiState.getAmountState(isFromConfirmation) ?: return null