Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-25 15:40:17 +03:00
parent b14caba3cf
commit a2d1834687
17 changed files with 285 additions and 299 deletions

View file

@ -22,6 +22,24 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.*
import java.text.DecimalFormat
/**
* Simple text field for amount input.
* Validates and trims input text using [DecimalFormat]. Formats visual output using [AmountVisualTransformation].
* Can display aligned placeholder and currency symbol [symbol].
*
* @param value initial text
* @param decimals number of decimal places
* @param onValueChange callback
* @param textStyle text and placeholder styles
* @param modifier modifier
* @param symbol currency symbol
* @param color text color
* @param placeholderAlignment alignment of placeholder
* @param showPlaceholder show placeholder
* @param keyboardOptions keyboard options
*
* @see [SimpleTextField] for standard text field
*/
@Composable
fun AmountTextField(
value: String,

View file

@ -14,6 +14,7 @@ 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.SendAmountCurrencyConverter
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
@ -39,8 +40,25 @@ internal class SendStateFactory(
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) }
private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) }
private val amountFieldConverter by lazy {
SendAmountFieldConverter(
clickIntents = clickIntents,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
)
}
private val amountFieldChangeConverter by lazy {
SendAmountFieldChangeConverter(
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val amountCurrencyConverter by lazy {
SendAmountCurrencyConverter(
currentStateProvider = currentStateProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val amountStateConverter by lazy {
SendAmountStateConverter(
@ -57,11 +75,7 @@ internal class SendStateFactory(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val feeStateConverter by lazy {
SendFeeStateConverter(
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
private val feeStateConverter by lazy { SendFeeStateConverter() }
private val recipientListStateConverter by lazy {
SendRecipientListConverter(
@ -90,16 +104,7 @@ internal class SendStateFactory(
//region amount state clicks
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState {
val state = currentStateProvider()
val amountState = state.amountState ?: return state
return if (amountState.isFiatValue == isFiat) {
state
} else {
return state.copy(amountState = amountState.copy(isFiatValue = isFiat))
}
}
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
//endregion
//region recipient

View file

@ -6,8 +6,7 @@ import androidx.paging.PagingData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.event.StateEvent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
@ -46,15 +45,11 @@ internal sealed class SendStates {
data class AmountState(
override val type: SendUiStateType = SendUiStateType.Amount,
override val isPrimaryButtonEnabled: Boolean,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val appCurrency: AppCurrency,
val walletName: String,
val walletBalance: String,
val walletBalance: TextReference,
val tokenIconState: TokenIconState,
val isFiatValue: Boolean,
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
val amountTextField: SendTextField.Amount,
val amountValue: BigDecimal,
val amountTextField: SendTextField.AmountField,
) : SendStates()
/** Recipient state */
@ -72,14 +67,13 @@ internal sealed class SendStates {
data class FeeState(
override val type: SendUiStateType = SendUiStateType.Fee,
override val isPrimaryButtonEnabled: Boolean = false,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val feeSelectorState: FeeSelectorState = FeeSelectorState.Loading,
val isSubtract: Boolean = false,
val isUserSubtracted: Boolean = false,
val fee: Fee? = null,
val receivedAmountValue: BigDecimal = BigDecimal.ZERO,
val receivedAmount: String = "",
val notifications: ImmutableList<SendFeeNotification> = persistentListOf(),
val feeSelectorState: FeeSelectorState,
val isSubtract: Boolean,
val isUserSubtracted: Boolean,
val fee: Fee?,
val receivedAmountValue: BigDecimal,
val receivedAmount: String,
val notifications: ImmutableList<SendFeeNotification>,
) : SendStates()
/** Send state */

View file

@ -1,18 +1,20 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
internal class SendAmountStateConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
@ -30,15 +32,11 @@ internal class SendAmountStateConverter(
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
return SendStates.AmountState(
appCurrency = appCurrency,
cryptoCurrencyStatus = status,
walletName = userWallet.name,
walletBalance = "$crypto ($fiat)",
walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)),
tokenIconState = iconStateConverter.convert(status),
amountTextField = sendAmountFieldConverter.convert(Unit),
isFiatValue = false,
isPrimaryButtonEnabled = false,
amountValue = BigDecimal.ZERO,
segmentedButtonConfig = persistentListOf(
SendAmountSegmentedButtonsConfig(
title = stringReference(status.currency.symbol),

View file

@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.features.send.impl.presentation.state.SendUiState
import java.math.BigDecimal
@ -9,7 +10,7 @@ import java.math.BigDecimal
* Calculate receiving amount when fee is subtracted from sending amount
*/
internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDecimal {
val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
val fee = feeAmount.amount.value ?: return BigDecimal.ZERO
return amountValue.minus(fee)
}
@ -25,8 +26,7 @@ internal fun FeeSelectorState.Content.getFee(): Fee {
FeeType.MARKET -> fees.normal
FeeType.FAST -> fees.priority
FeeType.CUSTOM -> {
val feeAmount =
customValues.firstOrNull()?.value?.let { BigDecimal(it.ifEmpty { "0" }) } ?: BigDecimal.ZERO
val feeAmount = customValues.firstOrNull()?.value.toBigDecimalOrDefault()
Fee.Common(
fees.normal.amount.copy(
value = feeAmount,

View file

@ -76,7 +76,7 @@ internal class FeeStateFactory(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountValue,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
),
@ -92,7 +92,7 @@ internal class FeeStateFactory(
feeState = updatedState?.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountValue,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
),
)
@ -119,7 +119,7 @@ internal class FeeStateFactory(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountValue,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
),
@ -150,7 +150,7 @@ internal class FeeStateFactory(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountValue,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
),
@ -174,7 +174,7 @@ internal class FeeStateFactory(
feeState = updatedState.copy(
notifications = feeNotificationFactory(
feeState = updatedState,
amountValue = state.amountState?.amountValue,
amountValue = state.amountState?.amountTextField?.cryptoAmount?.value,
),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled(),
),
@ -203,7 +203,7 @@ internal class FeeStateFactory(
private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean {
val feeState = state.feeState ?: return false
val amountValue = state.amountState?.amountValue ?: BigDecimal.ZERO
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
val feeAmount = fee.amount.value ?: BigDecimal.ZERO
return if (feeState.isUserSubtracted) {
feeState.isSubtract

View file

@ -4,13 +4,16 @@ 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.transaction.Fee
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
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 com.tangem.utils.toFormattedString
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -31,16 +34,24 @@ internal class SendFeeCustomFieldConverter(
return persistentListOf(
SendTextField.CustomFee(
value = ethereumFee.amount.value.toString(),
value = ethereumFee.amount.value?.toFormattedString(ethereumFee.amount.decimals).orEmpty(),
decimals = ethereumFee.amount.decimals,
symbol = ethereumFee.amount.currencySymbol,
onValueChange = { clickIntents.onCustomFeeValueChange(0, it) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Number,
),
label = TextReference.Str(maxFeeFiat),
title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_max_fee_footer),
label = stringReference(maxFeeFiat),
),
SendTextField.CustomFee(
value = ethereumFee.gasPrice.toString(),
decimals = 0,
symbol = ETHEREUM_UNIT,
title = resourceReference(R.string.send_gas_price),
footer = resourceReference(R.string.send_gas_price_footer),
onValueChange = { clickIntents.onCustomFeeValueChange(1, it) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
@ -49,6 +60,10 @@ internal class SendFeeCustomFieldConverter(
),
SendTextField.CustomFee(
value = ethereumFee.gasLimit.toString(),
decimals = 0,
symbol = null,
title = resourceReference(R.string.send_gas_limit),
footer = resourceReference(R.string.send_gas_limit_footer),
onValueChange = { clickIntents.onCustomFeeValueChange(2, it) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
@ -57,4 +72,8 @@ internal class SendFeeCustomFieldConverter(
),
)
}
companion object {
private const val ETHEREUM_UNIT = "GWEI"
}
}

View file

@ -1,17 +1,21 @@
package com.tangem.features.send.impl.presentation.state.fee
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
internal class SendFeeStateConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Unit, SendStates.FeeState> {
internal class SendFeeStateConverter : Converter<Unit, SendStates.FeeState> {
override fun convert(value: Unit): SendStates.FeeState {
return SendStates.FeeState(
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
feeSelectorState = FeeSelectorState.Loading,
isSubtract = false,
isUserSubtracted = false,
fee = null,
receivedAmountValue = BigDecimal.ZERO,
receivedAmount = "",
notifications = persistentListOf(),
)
}
}

View file

@ -1,59 +1,49 @@
package com.tangem.features.send.impl.presentation.state.fields
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
import java.text.NumberFormat
import java.math.RoundingMode
internal class SendAmountFieldChangeConverter(
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<String, SendUiState> {
override fun convert(value: String): SendUiState {
val state = currentStateProvider()
val amountState = state.amountState ?: return state
val amountTextField = amountState.amountTextField
val feeState = state.feeState ?: return state
if (value.checkDecimalSeparatorDuplicate()) return state
if (value.isEmpty()) return state.emptyState()
val fiatRate = amountState.cryptoCurrencyStatus.value.fiatRate
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
val trimmedValue = value.trim()
val cryptoValue = if (amountState.isFiatValue) {
if (value.isNotBlank()) {
trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
} else {
DEFAULT_VALUE
}
} else {
trimmedValue
}
val cryptoValue = trimmedValue.getCryptoValue(amountTextField.isFiatValue, cryptoDecimals)
val fiatValue = trimmedValue.getFiatValue(amountTextField.isFiatValue, fiatDecimals)
val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals)
val decimalFiatValue = fiatValue.parseToBigDecimal(fiatDecimals)
val fiatValue = if (!amountState.isFiatValue) {
if (value.isNotBlank()) {
trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty()
} else {
DEFAULT_VALUE
}
} else {
trimmedValue
}
val checkValue = if (amountState.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(amountState)
val isMaxAmount = checkValue.checkMaxAmount(amountState)
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(amountTextField)
val isMaxAmount = checkValue.checkMaxAmount(amountTextField)
return state.copy(
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance,
amountTextField = amountState.amountTextField.copy(
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
),
amountValue = cryptoValue.toBigDecimalOrDefault(),
),
feeState = feeState.copy(
isSubtract = isMaxAmount,
@ -61,58 +51,63 @@ internal class SendAmountFieldChangeConverter(
)
}
private fun String.getCryptoValue(isFiatValue: Boolean, decimals: Int): String {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val fiatRate = cryptoCurrencyStatus.value.fiatRate
return if (isFiatValue && fiatRate != null) {
parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN)
.parseBigDecimal(decimals)
} else {
this
}
}
private fun String.getFiatValue(isFiatValue: Boolean, decimals: Int): String {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val fiatRate = cryptoCurrencyStatus.value.fiatRate
return if (!isFiatValue && fiatRate != null) {
parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals)
} else {
this
}
}
private fun SendUiState.emptyState(): SendUiState {
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,
value = "",
fiatValue = "",
isError = false,
),
),
)
}
private fun String.checkDecimalSeparatorDuplicate(): Boolean {
val regex = TRIM_REGEX.toRegex()
val decimalSeparatorCount = regex.findAll(this).count()
return decimalSeparatorCount > 1
}
private fun String.checkExceedBalance(state: SendStates.AmountState): Boolean {
val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
return if (state.isFiatValue) {
toBigDecimal() > currencyFiatAmount
private fun String.checkExceedBalance(amountTextField: SendTextField.AmountField): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
return if (amountTextField.isFiatValue) {
parseToBigDecimal(amountTextField.fiatAmount.decimals) > currencyFiatAmount
} else {
toBigDecimal() > currencyCryptoAmount
parseToBigDecimal(amountTextField.cryptoAmount.decimals) > currencyCryptoAmount
}
}
private fun String.checkMaxAmount(state: SendStates.AmountState): Boolean {
private fun String.checkMaxAmount(amountTextField: SendTextField.AmountField): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
// If current currency is Token
if (state.cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false
if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false
val currencyCryptoAmount = state.cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val currencyFiatAmount = state.cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
return if (state.isFiatValue) {
toBigDecimal() == currencyFiatAmount
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
return if (amountTextField.isFiatValue) {
parseToBigDecimal(amountTextField.fiatAmount.decimals) == currencyFiatAmount
} else {
toBigDecimal() == currencyCryptoAmount
parseToBigDecimal(amountTextField.cryptoAmount.decimals) == currencyCryptoAmount
}
}
private fun String.trim(): String {
var trimmedValue = this
if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1)
return trimmedValue.replace(TRIM_REGEX.toRegex(), ".")
}
companion object {
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
private const val TRIM_REGEX = "[.,]"
}
}

View file

@ -4,31 +4,47 @@ 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.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import java.text.NumberFormat
import java.math.BigDecimal
private const val FIAT_DECIMALS = 2
internal class SendAmountFieldConverter(
private val clickIntents: SendClickIntents,
) : Converter<Unit, SendTextField.Amount> {
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Unit, SendTextField.AmountField> {
override fun convert(value: Unit): SendTextField.Amount {
return SendTextField.Amount(
override fun convert(value: Unit): SendTextField.AmountField {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
return SendTextField.AmountField(
value = "",
fiatValue = DEFAULT_VALUE,
fiatValue = "",
onValueChange = clickIntents::onAmountValueChange,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Number,
),
placeholder = TextReference.Str(DEFAULT_VALUE),
isFiatValue = false,
cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrencyStatus.currency),
fiatAmount = getAppCurrencyAmount(appCurrencyProvider()),
isError = false,
error = TextReference.Res(R.string.swapping_insufficient_funds),
)
}
companion object {
private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00)
}
private fun getAppCurrencyAmount(appCurrency: AppCurrency) = Amount(
currencySymbol = appCurrency.symbol,
value = BigDecimal.ZERO,
decimals = FIAT_DECIMALS,
type = AmountType.FiatType(appCurrency.code),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fields
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.Amount
@Immutable
internal sealed class SendTextField {
@ -16,14 +17,13 @@ internal sealed class SendTextField {
/** Keyboard options */
abstract val keyboardOptions: KeyboardOptions
// /** Placeholder (hint) */
// abstract val placeholder: TextReference
data class Amount(
data class AmountField(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
val placeholder: TextReference,
val cryptoAmount: Amount,
val fiatAmount: Amount,
val isFiatValue: Boolean,
val fiatValue: String,
val isError: Boolean,
val error: TextReference,
@ -53,6 +53,10 @@ internal sealed class SendTextField {
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
val symbol: String?,
val decimals: Int,
val title: TextReference,
val footer: TextReference,
val label: TextReference? = null,
) : SendTextField()
}

View file

@ -3,48 +3,49 @@ package com.tangem.features.send.impl.presentation.ui.amount
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.components.fields.AmountTextField
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.defaultFormat
import com.tangem.core.ui.utils.rememberDecimalFormat
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
@Composable
internal fun ColumnScope.AmountField(
sendField: SendTextField.Amount,
cryptoSymbol: String,
fiatSymbol: String,
isFiat: Boolean,
) {
val value = if (isFiat) sendField.fiatValue else sendField.value
val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value
val symbol = if (isFiat) fiatSymbol else cryptoSymbol
val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol
internal fun AmountField(sendField: SendTextField.AmountField, isFiat: Boolean) {
val decimalFormat = rememberDecimalFormat()
val (primaryValue, secondaryValue) = if (isFiat) {
sendField.fiatValue to sendField.value
} else {
sendField.value to sendField.fiatValue
}
AmountFieldInner(
value = value,
placeholder = sendField.placeholder,
symbol = symbol,
val (primaryAmount, secondaryAmount) = if (!isFiat) {
sendField.cryptoAmount to sendField.fiatAmount
} else {
sendField.fiatAmount to sendField.cryptoAmount
}
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
symbol = primaryAmount.currencySymbol,
onValueChange = sendField.onValueChange,
keyboardOptions = sendField.keyboardOptions,
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
placeholderAlignment = TopCenter,
modifier = Modifier
.align(CenterHorizontally)
.padding(
top = TangemTheme.dimens.spacing24,
start = TangemTheme.dimens.spacing12,
@ -54,15 +55,15 @@ internal fun ColumnScope.AmountField(
Box(
modifier = Modifier
.align(CenterHorizontally)
.padding(
top = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
) {
val text = "${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
Text(
text = "$secondaryValue $secondarySymbol",
text = text,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
@ -80,47 +81,6 @@ internal fun ColumnScope.AmountField(
}
}
@Composable
private fun AmountFieldInner(
value: String,
placeholder: TextReference,
symbol: String,
onValueChange: (String) -> Unit,
keyboardOptions: KeyboardOptions,
modifier: Modifier = Modifier,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier
.focusRequester(focusRequester)
.background(TangemTheme.colors.background.action),
textStyle = TangemTheme.typography.h2.copy(
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
),
keyboardOptions = keyboardOptions,
singleLine = true,
visualTransformation = AmountVisualTransformation(symbol),
decorationBox = { innerTextField ->
Box {
if (value.isBlank()) {
Text(
text = "${placeholder.resolveReference()} $symbol",
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.disabled,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.TopCenter),
)
}
innerTextField()
}
},
)
}
@Composable
private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) {
AnimatedVisibility(

View file

@ -12,12 +12,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.extensions.resolveReference
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) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.padding(
@ -33,29 +35,24 @@ internal fun AmountFieldContainer(amountState: SendStates.AmountState, modifier:
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing14)
.align(Alignment.CenterHorizontally),
.padding(top = TangemTheme.dimens.spacing14),
)
Text(
text = amountState.walletBalance,
text = amountState.walletBalance.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2)
.align(Alignment.CenterHorizontally),
.padding(top = TangemTheme.dimens.spacing2),
)
TokenIcon(
state = amountState.tokenIconState,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32)
.align(Alignment.CenterHorizontally),
.padding(top = TangemTheme.dimens.spacing32),
)
AmountField(
sendField = amountState.amountTextField,
isFiat = amountState.isFiatValue,
cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol,
fiatSymbol = amountState.appCurrency.symbol,
isFiat = amountState.amountTextField.isFiatValue,
)
}
}

View file

@ -5,86 +5,64 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.components.inputrow.InputRowEnter
import com.tangem.core.ui.components.inputrow.InputRowEnterInfo
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.inputrow.InputRowEnterAmount
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import kotlinx.collections.immutable.ImmutableList
private const val ETHEREUM_UNIT = "GWEI"
@Composable
internal fun SendCustomFeeEthereum(
customValues: ImmutableList<SendTextField.CustomFee>,
selectedFee: FeeType,
symbol: String,
modifier: Modifier = Modifier,
) {
if (selectedFee == FeeType.CUSTOM && customValues.isNotEmpty()) {
val fee = customValues[0]
val gasPrice = customValues[1]
val gasLimit = customValues[2]
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = modifier,
) {
FooterContainer(
footer = stringResource(R.string.send_max_fee_footer),
) {
InputRowEnterInfo(
text = fee.value,
title = TextReference.Res(R.string.send_max_fee),
info = fee.label,
visualTransformation = AmountVisualTransformation(symbol),
keyboardOptions = fee.keyboardOptions,
onValueChange = fee.onValueChange,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
FooterContainer(
footer = stringResource(R.string.send_gas_price_footer),
) {
InputRowEnter(
text = gasPrice.value,
title = TextReference.Res(R.string.send_gas_price),
onValueChange = gasPrice.onValueChange,
visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT),
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
FooterContainer(
footer = stringResource(R.string.send_gas_limit_footer),
) {
InputRowEnter(
text = gasLimit.value,
title = TextReference.Res(R.string.send_gas_limit),
onValueChange = gasLimit.onValueChange,
keyboardOptions = fee.keyboardOptions,
isSingleLine = true,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
repeat(customValues.size) { index ->
val value = customValues[index]
FooterContainer(
footer = value.footer.resolveReference(),
) {
if (value.label != null) {
InputRowEnterInfoAmount(
text = value.value,
decimals = value.decimals,
symbol = value.symbol,
title = value.title,
info = value.label,
keyboardOptions = value.keyboardOptions,
onValueChange = value.onValueChange,
showDivider = false,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
} else {
InputRowEnterAmount(
text = value.value,
decimals = value.decimals,
title = value.title,
symbol = value.symbol,
onValueChange = value.onValueChange,
keyboardOptions = value.keyboardOptions,
showDivider = false,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}
}
}
}

View file

@ -43,10 +43,7 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S
clickIntents = clickIntents,
)
}
customFee(
feeSendState = feeSendState,
cryptoCurrencySymbol = state.cryptoCurrencyStatus.currency.symbol,
)
customFee(feeSendState)
notifications(notifications)
subtractButton(
receivedAmount = state.receivedAmount,
@ -84,11 +81,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<SendFeeNotificat
}
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.customFee(
feeSendState: FeeSelectorState,
cryptoCurrencySymbol: String,
modifier: Modifier = Modifier,
) {
internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: Modifier = Modifier) {
item(
key = FEE_CUSTOM_KEY,
) {
@ -104,7 +97,6 @@ internal fun LazyListScope.customFee(
SendCustomFeeEthereum(
customValues = customValues,
selectedFee = fee.selectedFee,
symbol = cryptoCurrencySymbol,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
}

View file

@ -17,16 +17,17 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import com.tangem.blockchain.extensions.toBigDecimalOrDefault
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImage
import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.features.send.impl.R
import com.tangem.features.send.impl.presentation.state.SendNotification
import com.tangem.features.send.impl.presentation.state.SendStates
@ -61,7 +62,7 @@ internal fun SendContent(uiState: SendUiState) {
AnimatedVisibility(visible = !isSuccess) {
FromWallet(
walletName = amountState.walletName,
walletBalance = amountState.walletBalance,
walletBalance = amountState.walletBalance.resolveReference(),
)
}
AmountBlock(
@ -122,13 +123,14 @@ private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: Boolean,
val amount = amountState.amountTextField
val cryptoAmount = formatCryptoAmount(
cryptoCurrency = amountState.cryptoCurrencyStatus.currency,
cryptoAmount = amount.value.toBigDecimalOrDefault(),
cryptoAmount = amount.cryptoAmount.value,
cryptoCurrency = amount.cryptoAmount.currencySymbol,
decimals = amount.cryptoAmount.decimals,
)
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount.fiatValue.toBigDecimalOrDefault(),
fiatCurrencyCode = amountState.appCurrency.code,
fiatCurrencySymbol = amountState.appCurrency.symbol,
fiatAmount = amount.fiatAmount.value,
fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code,
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
)
InputRowImage(
title = TextReference.Res(R.string.send_amount_label),

View file

@ -11,6 +11,8 @@ import com.tangem.blockchain.blockchains.xrp.XrpAddressService
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.extensions.isZero
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.LegacyAction
@ -351,12 +353,15 @@ internal class SendViewModel @Inject constructor(
override fun onMaxValueClick() {
val amountState = uiState.amountState ?: return
val amount = if (amountState.isFiatValue) {
amountState.cryptoCurrencyStatus.value.fiatAmount
val amountTextField = amountState.amountTextField
val (amount, decimals) = if (amountTextField.isFiatValue) {
cryptoCurrencyStatus.value.fiatAmount to amountTextField.fiatAmount.decimals
} else {
amountState.cryptoCurrencyStatus.value.amount
cryptoCurrencyStatus.value.amount to amountTextField.cryptoAmount.decimals
}
if (amount != null && !amount.isZero()) {
onAmountValueChange(amount.parseBigDecimal(decimals))
}
onAmountValueChange(amount?.toPlainString() ?: DEFAULT_VALUE)
}
// endregion
@ -451,7 +456,7 @@ internal class SendViewModel @Inject constructor(
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
val amountState = uiState.amountState ?: return null
val recipientState = uiState.recipientState ?: return null
val amount = amountState.amountTextField.value.toBigDecimal()
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
return getFeeUseCase.invoke(
amount = amount,
@ -495,7 +500,7 @@ internal class SendViewModel @Inject constructor(
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
val memo = uiState.recipientState?.memoTextField?.value
val fee = feeSelectorState.getFee()
val amountValue = uiState.amountState?.amountValue ?: return
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
val amountToSend = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue
@ -616,7 +621,6 @@ internal class SendViewModel @Inject constructor(
companion object {
private const val XRP_X_ADDRESS = 'X'
private const val DEFAULT_VALUE = "0.00"
private const val CHECK_FEE_UPDATE_DELAY = 60_000L
private const val BALANCE_UPDATE_DELAY = 10_000L
}