Updated on 2026-08-14
This commit is contained in:
parent
8b0f0e589e
commit
f94370c41c
7 changed files with 139 additions and 62 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue