Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-24 14:17:37 +05:00
commit 0b8efe6d02
3 changed files with 211 additions and 190 deletions

View file

@ -1,18 +1,11 @@
package com.tangem.features.send.impl.presentation.state
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.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.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
@ -20,18 +13,14 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientWalletListConverter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import timber.log.Timber
import java.math.BigDecimal
@Suppress("LongParameterList", "LargeClass")
@Suppress("LongParameterList")
internal class SendStateFactory(
private val clickIntents: SendClickIntents,
private val stateRouterProvider: Provider<StateRouter>,
@ -41,7 +30,6 @@ internal class SendStateFactory(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val isTapHelpPreviewEnabledProvider: Provider<Boolean>,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
@ -79,14 +67,6 @@ internal class SendStateFactory(
isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider,
)
}
private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientWalletListConverter()
}
private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientHistoryListConverter(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val sendSyncEditConverter by lazy(LazyThreadSafetyMode.NONE) {
SendSyncEditConverter(currentStateProvider = currentStateProvider)
}
@ -132,159 +112,6 @@ internal class SendStateFactory(
}
//endregion
//region recipient
fun onLoadedWalletsList(wallets: List<AvailableWallet?>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
wallets = recipientWalletListStateConverter.convert(wallets),
),
)
}
fun onLoadedHistoryList(txHistory: List<TxHistoryItem>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
recent = recipientHistoryListStateConverter.convert(txHistory),
),
)
}
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false, isValuePasted: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
addressTextField = recipientState.addressTextField.copy(value = value, isValuePasted = isValuePasted),
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
),
)
}
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = recipientState.memoTextField?.value.orEmpty(),
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
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,
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,
),
),
)
}
fun getOnRecipientAddressValidationStarted(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(isValidating = true),
)
}
fun getOnRecipientMemoValueChange(value: String, isValuePasted: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = value,
isValuePasted = isValuePasted,
),
),
)
}
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = value,
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
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,
isValidating = false,
memoTextField = recipientState.memoTextField?.copy(
isError = value.isNotEmpty() && !isValidMemo,
isEnabled = true,
),
),
)
}
fun getOnXAddressMemoState(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = "",
isEnabled = false,
),
),
)
}
fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: 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(
recent = recipientState.recent.map { recent ->
recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY))
}.toPersistentList(),
wallets = recipientState.wallets.map { wallet ->
wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY))
}.toPersistentList(),
),
)
}
//endregion
//region send
fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()

View file

@ -0,0 +1,182 @@
package com.tangem.features.send.impl.presentation.state.recipient
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.features.send.impl.R
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import kotlinx.collections.immutable.toPersistentList
import timber.log.Timber
internal class RecipientSendFactory(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
) {
private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientWalletListConverter()
}
private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendRecipientHistoryListConverter(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
fun onLoadedWalletsList(wallets: List<AvailableWallet?>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
wallets = recipientWalletListStateConverter.convert(wallets),
),
)
}
fun onLoadedHistoryList(txHistory: List<TxHistoryItem>): SendUiState {
val state = currentStateProvider()
return state.copy(
recipientState = state.recipientState?.copy(
recent = recipientHistoryListStateConverter.convert(txHistory),
),
)
}
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false, isValuePasted: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
addressTextField = recipientState.addressTextField.copy(value = value, isValuePasted = isValuePasted),
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
),
)
}
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = recipientState.memoTextField?.value.orEmpty(),
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
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,
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,
),
),
)
}
fun getOnRecipientAddressValidationStarted(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(isValidating = true),
)
}
fun getOnRecipientMemoValueChange(value: String, isValuePasted: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = value,
isValuePasted = isValuePasted,
),
),
)
}
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
val isValidMemo = validateWalletMemoUseCase(
memo = value,
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
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,
isValidating = false,
memoTextField = recipientState.memoTextField?.copy(
isError = value.isNotEmpty() && !isValidMemo,
isEnabled = true,
),
),
)
}
fun getOnXAddressMemoState(): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val recipientState = state.getRecipientState(isEditState) ?: return state
return state.copyWrapped(
isEditState = isEditState,
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(
value = "",
isEnabled = false,
),
),
)
}
fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: 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(
recent = recipientState.recent.map { recent ->
recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY))
}.toPersistentList(),
wallets = recipientState.wallets.map { wallet ->
wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY))
}.toPersistentList(),
),
)
}
}

View file

@ -50,9 +50,13 @@ import com.tangem.features.send.impl.presentation.state.*
import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
import com.tangem.features.send.impl.presentation.state.fee.*
import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendFactory
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
@ -128,10 +132,16 @@ internal class SendViewModel @Inject constructor(
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
validateWalletMemoUseCase = validateWalletMemoUseCase,
isTapHelpPreviewEnabledProvider = Provider { isTapHelpPreviewEnabled },
)
private val recipientStateFactory = RecipientSendFactory(
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
validateWalletMemoUseCase = validateWalletMemoUseCase,
)
private val amountStateFactory = AmountStateFactory(
stateRouterProvider = Provider { stateRouter },
currentStateProvider = Provider { uiState },
@ -407,9 +417,9 @@ internal class SendViewModel @Inject constructor(
.orEmpty()
}.onSuccess { result ->
userWallets = result
uiState = stateFactory.onLoadedWalletsList(wallets = userWallets)
uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
}.onFailure {
uiState = stateFactory.onLoadedWalletsList(wallets = emptyList())
uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList())
}
}
}
@ -449,7 +459,7 @@ internal class SendViewModel @Inject constructor(
userWalletId = userWalletId,
currency = cryptoCurrency,
).getOrElse { emptyList() }
uiState = stateFactory.onLoadedHistoryList(txHistory = txHistoryList)
uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
}
private fun onStateActive() {
@ -609,10 +619,10 @@ internal class SendViewModel @Inject constructor(
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
viewModelScope.launch {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) }
autoNextFromRecipient(type, isValidAddress)
}
@ -622,10 +632,11 @@ internal class SendViewModel @Inject constructor(
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
viewModelScope.launch {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
uiState = stateFactory.getOnRecipientAddressValidationStarted()
val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty())
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
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)
}
}.saveIn(memoValidationJobHolder)
}
@ -644,16 +655,17 @@ internal class SendViewModel @Inject constructor(
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
uiState = stateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true)
uiState = stateFactory.getOnXAddressMemoState()
uiState =
recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true)
uiState = recipientStateFactory.getOnXAddressMemoState()
val isValidAddress = validateAddress(decodedAddress.address)
uiState = stateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
true
} ?: false
}
private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) {
uiState = stateFactory.getHiddenRecentListState(
uiState = recipientStateFactory.getHiddenRecentListState(
isAddressInWallet = isAddressInWallet,
isValidAddress = isValidAddress,
)