Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-15 19:24:38 +03:00
parent b2129017a6
commit d8ce00bd6c
19 changed files with 278 additions and 121 deletions

View file

@ -4,7 +4,7 @@ import androidx.paging.PagingData
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
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
@ -23,7 +23,6 @@ import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientS
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.features.send.impl.presentation.viewmodel.isNotAddressInWallet
import com.tangem.features.send.impl.presentation.viewmodel.validateMemo
import com.tangem.features.send.impl.presentation.viewmodel.verifyAddress
import com.tangem.utils.Provider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
@ -118,41 +117,64 @@ internal class SendStateFactory(
)
}
fun getOnRecipientAddressValueChangeState(value: String): SendUiState {
fun onRecipientAddressValueChange(value: String): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
recipientState = recipientState.copy(
addressTextField = recipientState.addressTextField.copy(value = value),
),
)
}
fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
val isValidMemo = validateMemo(
memo = recipientState.addressTextField.value.value,
memo = recipientState.addressTextField.value,
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
val isAddressInWallet = isNotAddressInWallet(
address = value,
walletAddresses = walletAddressesProvider(),
)
val isValidAddress = verifyAddress(
address = value,
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
recipientState.addressTextField.update {
it.copy(
value = value,
error = when {
!isValidAddress || !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error)
else -> null
},
isError = !isValidAddress || !isAddressInWallet,
)
}
return state.copy(
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
isValidating = false,
addressTextField = recipientState.addressTextField.copy(
error = when {
!isValidAddress || !isAddressInWallet -> resourceReference(
R.string.send_recipient_address_error,
)
else -> null
},
isError = value.isNotEmpty() && !isValidAddress || !isAddressInWallet,
),
),
)
}
fun getOnRecipientMemoValueChangeState(value: String): SendUiState {
fun getOnRecipientAddressValidationStarted(): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
recipientState = recipientState.copy(isValidating = true),
)
}
fun getOnRecipientMemoValueChange(value: String): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
return state.copy(
recipientState = recipientState.copy(
memoTextField = recipientState.memoTextField?.copy(value = value),
),
)
}
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
val state = currentStateProvider()
val recipientState = state.recipientState ?: return state
@ -162,22 +184,15 @@ internal class SendStateFactory(
)
val isAddressInWallet = isNotAddressInWallet(
walletAddresses = walletAddressesProvider(),
address = recipientState.addressTextField.value.value,
address = recipientState.addressTextField.value,
)
val isValidAddress = verifyAddress(
address = recipientState.addressTextField.value.value,
cryptoCurrency = cryptoCurrencyStatusProvider().currency,
)
recipientState.memoTextField?.update {
it.copy(
value = value,
isError = !isValidMemo,
)
}
return state.copy(
recipientState = recipientState.copy(
isPrimaryButtonEnabled = isValidMemo && isValidAddress && isAddressInWallet,
isValidating = false,
memoTextField = recipientState.memoTextField?.copy(
isError = value.isNotEmpty() || isValidMemo && isValidAddress && isAddressInWallet,
),
),
)
}

View file

@ -43,18 +43,19 @@ internal sealed class SendStates {
val tokenIconState: TokenIconState,
val isFiatValue: Boolean,
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
val amountTextField: MutableStateFlow<SendTextField.Amount>,
val amountTextField: SendTextField.Amount,
val isPrimaryButtonEnabled: Boolean,
) : SendStates()
/** Recipient state */
data class RecipientState(
override val type: SendUiStateType = SendUiStateType.Recipient,
val addressTextField: MutableStateFlow<SendTextField.RecipientAddress>,
val memoTextField: MutableStateFlow<SendTextField.RecipientMemo>?,
val addressTextField: SendTextField.RecipientAddress,
val memoTextField: SendTextField.RecipientMemo?,
val recipients: MutableStateFlow<PagingData<SendRecipientListContent>> = MutableStateFlow(PagingData.empty()),
val network: String,
val isPrimaryButtonEnabled: Boolean,
val isValidating: Boolean = false,
) : SendStates()
/** Fee and speed state */

View file

@ -12,7 +12,6 @@ import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldCo
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
internal class SendAmountStateConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
@ -35,7 +34,7 @@ internal class SendAmountStateConverter(
walletName = userWallet.name,
walletBalance = "$crypto ($fiat)",
tokenIconState = iconStateConverter.convert(status),
amountTextField = MutableStateFlow(sendAmountFieldConverter.convert(Unit)),
amountTextField = sendAmountFieldConverter.convert(Unit),
isFiatValue = false,
isPrimaryButtonEnabled = false,
segmentedButtonConfig = persistentListOf(

View file

@ -5,7 +5,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.coroutines.flow.update
import java.text.DecimalFormatSymbols
import java.text.NumberFormat
@ -42,31 +41,27 @@ internal class SendAmountFieldChangeConverter(
}
val isExceedBalance = value.checkExceedBalance(amountState.cryptoCurrencyStatus, amountState)
amountState.amountTextField.update {
it.copy(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
)
}
return state.copy(
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance,
amountTextField = amountState.amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
),
),
)
}
private fun SendUiState.emptyState(): SendUiState {
amountState?.amountTextField?.update {
it.copy(
value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE,
fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE,
isError = false,
)
}
return copy(
amountState = amountState?.copy(
isPrimaryButtonEnabled = false,
amountTextField = amountState.amountTextField.copy(
value = if (!amountState.isFiatValue) "" else DEFAULT_VALUE,
fiatValue = if (amountState.isFiatValue) "" else DEFAULT_VALUE,
isError = false,
),
),
)
}

View file

@ -3,29 +3,27 @@ package com.tangem.features.send.impl.presentation.state.recipient
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.coroutines.flow.MutableStateFlow
internal class SendRecipientAddressFieldConverter(
private val clickIntents: SendClickIntents,
) : Converter<Unit, MutableStateFlow<SendTextField.RecipientAddress>> {
) : Converter<Unit, SendTextField.RecipientAddress> {
override fun convert(value: Unit): MutableStateFlow<SendTextField.RecipientAddress> {
return MutableStateFlow(
SendTextField.RecipientAddress(
value = "",
onValueChange = clickIntents::onRecipientAddressValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Text,
),
placeholder = TextReference.Res(R.string.send_enter_address_field),
label = TextReference.Res(R.string.send_recipient),
override fun convert(value: Unit): SendTextField.RecipientAddress {
return SendTextField.RecipientAddress(
value = "",
onValueChange = clickIntents::onRecipientAddressValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Text,
),
error = resourceReference(R.string.send_recipient_address_error),
placeholder = resourceReference(R.string.send_enter_address_field),
label = resourceReference(R.string.send_recipient),
)
}
}

View file

@ -4,21 +4,20 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.coroutines.flow.MutableStateFlow
internal class SendRecipientMemoFieldConverter(
private val clickIntents: SendClickIntents,
private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>,
) : Converter<Int, MutableStateFlow<SendTextField.RecipientMemo>> {
) : Converter<Int, SendTextField.RecipientMemo> {
fun convertOrNull(): MutableStateFlow<SendTextField.RecipientMemo>? {
fun convertOrNull(): SendTextField.RecipientMemo? {
val cryptoCurrency = cryptoCurrencyStatus().currency
return when (cryptoCurrency.network.id.value) {
@ -34,19 +33,17 @@ internal class SendRecipientMemoFieldConverter(
}
}
override fun convert(value: Int): MutableStateFlow<SendTextField.RecipientMemo> {
return MutableStateFlow(
SendTextField.RecipientMemo(
value = "",
onValueChange = clickIntents::onRecipientMemoValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text,
),
placeholder = TextReference.Res(R.string.send_optional_field),
label = TextReference.Res(value),
error = TextReference.Res(R.string.send_memo_destination_tag_error),
override fun convert(value: Int): SendTextField.RecipientMemo {
return SendTextField.RecipientMemo(
value = "",
onValueChange = clickIntents::onRecipientMemoValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text,
),
placeholder = resourceReference(R.string.send_optional_field),
label = resourceReference(value),
error = resourceReference(R.string.send_memo_destination_tag_error),
)
}
}

View file

@ -11,14 +11,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.SendStates
@Composable
internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier: Modifier = Modifier) {
val amountTextField = amountState.amountTextField.collectAsStateWithLifecycle()
Column(
modifier = modifier
.fillMaxWidth()
@ -54,7 +52,7 @@ internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier:
.align(Alignment.CenterHorizontally),
)
AmountField(
sendField = amountTextField.value,
sendField = amountState.amountTextField,
isFiat = amountState.isFiatValue,
cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol,
fiatSymbol = amountState.appCurrency.symbol,

View file

@ -11,7 +11,9 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
@ -38,8 +40,9 @@ internal fun SendRecipientContent(
recipientList: LazyPagingItems<SendRecipientListContent>,
) {
if (uiState == null) return
val address = uiState.addressTextField.collectAsState().value
val memo = uiState.memoTextField?.collectAsState()?.value
val address = uiState.addressTextField
val isValidating by remember(uiState.isValidating) { derivedStateOf { uiState.isValidating } }
val isError by remember(address.isError) { derivedStateOf { address.isError } }
LazyColumn(
modifier = Modifier
.fillMaxSize()
@ -57,7 +60,8 @@ internal fun SendRecipientContent(
onValueChange = address.onValueChange,
onPasteClick = clickIntents::onRecipientAddressValueChange,
singleLine = true,
isError = address.isError,
isError = isError,
isLoading = isValidating,
error = address.error,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4)
@ -68,7 +72,7 @@ internal fun SendRecipientContent(
)
}
}
memo?.let { memoField ->
uiState.memoTextField?.let { memoField ->
item(key = MEMO_FIELD_KEY) {
TextFieldWithPaste(
value = memoField.value,

View file

@ -114,14 +114,14 @@ private fun FromWallet(walletName: String, walletBalance: String) {
@Composable
private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val amount = amountState.amountTextField.collectAsStateWithLifecycle()
val amount = amountState.amountTextField
val cryptoAmount = formatCryptoAmount(
cryptoCurrency = amountState.cryptoCurrencyStatus.currency,
cryptoAmount = amount.value.value.toBigDecimalOrDefault(),
cryptoAmount = amount.value.toBigDecimalOrDefault(),
)
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount.value.fiatValue.toBigDecimalOrDefault(),
fiatAmount = amount.fiatValue.toBigDecimalOrDefault(),
fiatCurrencyCode = amountState.appCurrency.code,
fiatCurrencySymbol = amountState.appCurrency.symbol,
)
@ -140,8 +140,8 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State<Bo
@Composable
private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State<Boolean>, onClick: () -> Unit) {
val address = recipientState.addressTextField.collectAsStateWithLifecycle()
val memo = recipientState.memoTextField?.collectAsStateWithLifecycle()
val address = recipientState.addressTextField
val memo = recipientState.memoTextField
Column(
modifier = Modifier
@ -149,16 +149,16 @@ private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess:
.background(TangemTheme.colors.background.action)
.clickable(enabled = !isSuccess.value) { onClick() },
) {
val showMemo = memo != null && memo.value.value.isNotBlank()
val showMemo = memo != null && memo.value.isNotBlank()
InputRowRecipientDefault(
title = TextReference.Res(R.string.send_recipient),
value = address.value.value,
value = address.value,
showDivider = showMemo,
)
if (showMemo) {
InputRowDefault(
title = TextReference.Res(R.string.send_extras_hint_memo),
text = TextReference.Str(memo?.value?.value.orEmpty()),
text = TextReference.Str(memo?.value.orEmpty()),
)
}
}

View file

@ -37,6 +37,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
@ -74,6 +75,7 @@ internal class SendViewModel @Inject constructor(
private val getFeeUseCase: GetFeeUseCase,
private val sendTransactionUseCase: SendTransactionUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
private val walletManagersFacade: WalletManagersFacade,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, SendClickIntents {
@ -110,6 +112,7 @@ internal class SendViewModel @Inject constructor(
private var recipientsJobHolder = JobHolder()
private var walletAddressesJobHolder = JobHolder()
private var feeJobHolder = JobHolder()
private var addressValidationJobHolder = JobHolder()
override fun onCreate(owner: LifecycleOwner) {
getWalletAddresses()
@ -255,8 +258,8 @@ internal class SendViewModel @Inject constructor(
stateFactory.onFeeOnLoadingState()
getFeeUseCase.invoke(
amount = amountState.amountTextField.value.value.toBigDecimal(),
destination = recipientState.addressTextField.value.value,
amount = amountState.amountTextField.value.toBigDecimal(),
destination = recipientState.addressTextField.value,
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
@ -319,15 +322,33 @@ internal class SendViewModel @Inject constructor(
// region recipient state clicks
override fun onRecipientAddressValueChange(value: String) {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.getOnRecipientAddressValueChangeState(value)
}
uiState = stateFactory.onRecipientAddressValueChange(value)
viewModelScope.launch(dispatchers.main) {
uiState = stateFactory.getOnRecipientAddressValidationStarted()
if (!checkIfXrpAddressValue(value)) {
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress)
}
}.saveIn(addressValidationJobHolder)
}
override fun onRecipientMemoValueChange(value: String) {
if (!checkIfXrpAddressValue(value)) {
uiState = stateFactory.getOnRecipientMemoValueChangeState(value)
}
uiState = stateFactory.getOnRecipientMemoValueChange(value)
viewModelScope.launch(dispatchers.main) {
uiState = stateFactory.getOnRecipientAddressValidationStarted()
if (!checkIfXrpAddressValue(value)) {
val isValidAddress = validateAddress(value)
uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress)
}
}.saveIn(addressValidationJobHolder)
}
private suspend fun validateAddress(value: String): Boolean {
return validateWalletAddressUseCase(
userWalletId = userWalletId,
network = cryptoCurrency.network,
address = value,
).getOrElse { false }
}
private fun checkIfXrpAddressValue(value: String): Boolean {
@ -395,7 +416,7 @@ internal class SendViewModel @Inject constructor(
is TransactionFee.Single -> selectedFee.normal.amount.value
} ?: BigDecimal.ZERO
return BigDecimal(amount.value).minus(fee)
return BigDecimal(amount).minus(fee)
}
//endregion
@ -418,7 +439,7 @@ internal class SendViewModel @Inject constructor(
val memo = uiState.recipientState?.memoTextField?.value
val fee = getFee(feeState) ?: return
val amountToSend = amount.value.toBigDecimal().convertToAmount(cryptoCurrency)
val amountToSend = amount.toBigDecimal().convertToAmount(cryptoCurrency)
// todo add notifications [[REDACTED_JIRA]]
// val transactionErrors = walletManagersFacade.validateTransaction(
@ -431,11 +452,11 @@ internal class SendViewModel @Inject constructor(
val txData = walletManagersFacade.createTransaction(
amount = amountToSend,
fee = fee,
memo = memo?.value,
destination = recipient.value,
memo = memo,
destination = recipient,
userWalletId = userWalletId,
network = cryptoCurrency.network,
)?.copy(extras = getMemoExtras(cryptoCurrency.network.id.value, memo?.value)) ?: return
)?.copy(extras = getMemoExtras(cryptoCurrency.network.id.value, memo)) ?: return
sendTransactionUseCase(
txData = txData,