Updated on 2026-08-14
This commit is contained in:
parent
f9ace58267
commit
9ac2009c89
14 changed files with 250 additions and 149 deletions
|
|
@ -1,10 +1,7 @@
|
|||
package com.tangem.core.ui.components.appbar
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
|
@ -66,8 +63,7 @@ fun AppBarWithBackButtonAndIconContent(
|
|||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.weight(1f)
|
||||
.animateContentSize(),
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = !text.isNullOrBlank(),
|
||||
|
|
@ -84,8 +80,8 @@ fun AppBarWithBackButtonAndIconContent(
|
|||
}
|
||||
AnimatedVisibility(
|
||||
visible = !subtitle.isNullOrBlank(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
enter = fadeIn().plus(expandVertically()),
|
||||
exit = fadeOut().plus(shrinkVertically()),
|
||||
label = "Toolbar subtitle change",
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -93,6 +89,7 @@ fun AppBarWithBackButtonAndIconContent(
|
|||
color = TangemTheme.colors.text.secondary,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.animateContentSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ package com.tangem.core.ui.components.fields
|
|||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.TopCenter
|
||||
|
|
@ -21,6 +19,7 @@ import androidx.compose.ui.text.ParagraphIntrinsics
|
|||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.createFontFamilyResolver
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -33,20 +32,21 @@ 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].
|
||||
* Validates and trims input text using [DecimalFormat]. Formats visual output using [visualTransformation].
|
||||
* Can display aligned placeholder.
|
||||
*
|
||||
* @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 visualTransformation text visual transformation
|
||||
* @param keyboardOptions keyboard options
|
||||
*
|
||||
* @param keyboardActions keyboard actions
|
||||
* @param isEnabled is field editing enabled
|
||||
* @param isAutoResize is text font auto resize
|
||||
* @param reduceFactor font resize factor
|
||||
* @see [SimpleTextField] for standard text field
|
||||
*/
|
||||
@Composable
|
||||
|
|
@ -56,10 +56,8 @@ fun AmountTextField(
|
|||
onValueChange: (String) -> Unit,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
symbol: String? = null,
|
||||
color: Color = TangemTheme.colors.text.primary1,
|
||||
placeholderAlignment: Alignment = TopStart,
|
||||
showPlaceholder: Boolean = true,
|
||||
visualTransformation: VisualTransformation = AmountVisualTransformation(decimals),
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
|
|
@ -70,12 +68,6 @@ fun AmountTextField(
|
|||
reduceFactor: Double = 0.9,
|
||||
) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val visualTransformation = remember { AmountVisualTransformation(decimals, symbol, decimalFormat) }
|
||||
val placeholderTextAlign = if (placeholderAlignment == TopCenter) {
|
||||
TextAlign.Center
|
||||
} else {
|
||||
TextAlign.Start
|
||||
}
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
var fontSize = textStyle.fontSize
|
||||
if (isAutoResize) {
|
||||
|
|
@ -96,6 +88,7 @@ fun AmountTextField(
|
|||
}
|
||||
}
|
||||
}
|
||||
val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = { newText ->
|
||||
|
|
@ -108,31 +101,13 @@ fun AmountTextField(
|
|||
fontSize = fontSize,
|
||||
textDirection = TextDirection.ContentOrLtr,
|
||||
),
|
||||
color = color,
|
||||
color = textColor,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = true,
|
||||
readOnly = !isEnabled,
|
||||
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
|
||||
visualTransformation = visualTransformation,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.action),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (value.isBlank() && showPlaceholder) {
|
||||
var placeholder = decimalFormat.defaultFormat()
|
||||
if (symbol != null) {
|
||||
placeholder = placeholder.plus(" $symbol")
|
||||
}
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = textStyle.copy(textDirection = TextDirection.ContentOrLtr),
|
||||
color = TangemTheme.colors.text.disabled,
|
||||
textAlign = placeholderTextAlign,
|
||||
modifier = Modifier.align(placeholderAlignment),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -153,9 +128,6 @@ private fun AmountTextFieldPreview(
|
|||
AmountTextField(
|
||||
value = text,
|
||||
decimals = amount.decimals,
|
||||
symbol = amount.symbol,
|
||||
placeholderAlignment = amount.placeholderAlignment,
|
||||
showPlaceholder = amount.showPlaceholder,
|
||||
onValueChange = { text = it },
|
||||
textStyle = TangemTheme.typography.h2.copy(
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -169,28 +141,24 @@ private fun AmountTextFieldPreview(
|
|||
private class AmountTextFieldPreviewProvider : PreviewParameterProvider<AmountTextFieldPreviewData> {
|
||||
override val values = sequenceOf(
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "USD",
|
||||
value = "1000000,123123",
|
||||
decimals = 3,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = true,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = "1000000.123123",
|
||||
decimals = 6,
|
||||
placeholderAlignment = TopStart,
|
||||
showPlaceholder = false,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = "$",
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
placeholderAlignment = TopCenter,
|
||||
),
|
||||
AmountTextFieldPreviewData(
|
||||
symbol = null,
|
||||
value = null,
|
||||
decimals = 2,
|
||||
showPlaceholder = true,
|
||||
|
|
@ -200,7 +168,6 @@ private class AmountTextFieldPreviewProvider : PreviewParameterProvider<AmountTe
|
|||
}
|
||||
|
||||
private data class AmountTextFieldPreviewData(
|
||||
val symbol: String? = "$",
|
||||
val value: String? = null,
|
||||
val decimals: Int = 2,
|
||||
val showPlaceholder: Boolean,
|
||||
|
|
|
|||
|
|
@ -1,48 +1,82 @@
|
|||
package com.tangem.core.ui.components.fields.visualtransformations
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.OffsetMapping
|
||||
import androidx.compose.ui.text.input.TransformedText
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class AmountVisualTransformation(
|
||||
private val decimals: Int,
|
||||
private val symbol: String? = null,
|
||||
private val currencyCode: String? = null,
|
||||
private val decimalFormat: DecimalFormat = DecimalFormat(),
|
||||
) : VisualTransformation {
|
||||
|
||||
override fun filter(text: AnnotatedString): TransformedText {
|
||||
val formattedText = decimalFormat.formatWithThousands(
|
||||
var formattedAmount = decimalFormat.formatWithThousands(
|
||||
text.text,
|
||||
decimals,
|
||||
)
|
||||
formattedAmount = formattedAmount.ifEmpty { decimalFormat.defaultFormat() }
|
||||
val decimalValue = text.text.parseToBigDecimal(decimals)
|
||||
val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) {
|
||||
AnnotatedString(
|
||||
if (currencyCode != null) {
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = decimalValue,
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = symbol,
|
||||
)
|
||||
} else {
|
||||
BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = decimalValue,
|
||||
cryptoSymbol = symbol,
|
||||
decimals = decimals,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
AnnotatedString(decimalFormat.defaultFormat())
|
||||
}
|
||||
|
||||
val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator
|
||||
return TransformedText(
|
||||
text = buildAnnotatedString {
|
||||
append(formattedText)
|
||||
if (formattedText.isNotEmpty() && symbol != null) {
|
||||
append(" $symbol")
|
||||
}
|
||||
},
|
||||
offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol),
|
||||
text = formattedText,
|
||||
offsetMapping = OffsetMappingImpl(text.text, formattedText, symbol, groupingSymbol),
|
||||
)
|
||||
}
|
||||
|
||||
private class OffsetMappingImpl(
|
||||
private val text: String,
|
||||
private val formattedText: String,
|
||||
private val formattedText: AnnotatedString,
|
||||
private val currencySymbol: String?,
|
||||
private val gropingSymbol: Char,
|
||||
) : OffsetMapping {
|
||||
override fun originalToTransformed(offset: Int): Int {
|
||||
var noneDigitCount = 0
|
||||
var i = 0
|
||||
val symbolOffset = currencySymbol?.let { formattedText.indexOf(it) } ?: -1
|
||||
while (i < offset + noneDigitCount) {
|
||||
if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++
|
||||
val char = formattedText.getOrNull(i++)
|
||||
if (char == gropingSymbol) noneDigitCount++
|
||||
if (symbolOffset == 0 && char?.isWhitespace() == true) noneDigitCount++
|
||||
}
|
||||
return (offset + noneDigitCount).coerceIn(0, formattedText.length)
|
||||
var transformedOffset = if (symbolOffset == 0 && currencySymbol != null) {
|
||||
currencySymbol.length + offset + noneDigitCount
|
||||
} else {
|
||||
offset + noneDigitCount
|
||||
}
|
||||
transformedOffset = if (symbolOffset > 0) {
|
||||
transformedOffset.coerceIn(0, minOf(symbolOffset, formattedText.length))
|
||||
} else {
|
||||
transformedOffset.coerceIn(0, formattedText.length)
|
||||
}
|
||||
return transformedOffset
|
||||
}
|
||||
|
||||
override fun transformedToOriginal(offset: Int): Int {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
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.rememberDecimalFormat
|
||||
|
||||
/**
|
||||
* Input row for entering amount. Manages correct amount format and validation
|
||||
|
|
@ -76,7 +78,11 @@ fun InputRowEnterAmount(
|
|||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
decimalFormat = rememberDecimalFormat(),
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
|
|
|
|||
|
|
@ -12,23 +12,28 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
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.rememberDecimalFormat
|
||||
|
||||
/**
|
||||
* `Input Row Enter Info` for entering amount. Manages correct amount format and validation
|
||||
|
||||
* @param title title reference
|
||||
* @param text primary text reference
|
||||
* @param decimals amount text decimal count
|
||||
* @param onValueChange text change callback
|
||||
* @param modifier modifier
|
||||
* @param symbol amount symbol
|
||||
* @param info info text
|
||||
* @param titleColor title color
|
||||
* @param textColor text color
|
||||
* @param isSingleLine text
|
||||
* @param visualTransformation applied transformation to text
|
||||
* @param infoColor info text color
|
||||
* @param keyboardOptions keyboard options for field
|
||||
* @param keyboardActions keyboard actions for field
|
||||
* @param showDivider show divider
|
||||
*
|
||||
* @see [InputRowEnterInfo]
|
||||
|
|
@ -71,7 +76,11 @@ fun InputRowEnterInfoAmount(
|
|||
AmountTextField(
|
||||
value = text,
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = decimals,
|
||||
symbol = symbol,
|
||||
decimalFormat = rememberDecimalFormat(),
|
||||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,30 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmountUncapped(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoSymbol: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = minOf(2, decimals)
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoSymbol.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009$cryptoSymbol"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ internal sealed class SendStates {
|
|||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||
val amountTextField: SendTextField.AmountField,
|
||||
val notifications: ImmutableList<SendNotification>,
|
||||
val appCurrencyCode: String,
|
||||
val isFeeLoading: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ internal class SendAmountStateConverter(
|
|||
isPrimaryButtonEnabled = false,
|
||||
notifications = persistentListOf(),
|
||||
isFeeLoading = false,
|
||||
appCurrencyCode = appCurrency.code,
|
||||
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
|
||||
persistentListOf()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ internal class EthereumCustomFeeConverter(
|
|||
),
|
||||
SendTextField.CustomFee(
|
||||
value = value.gasPrice.toString(),
|
||||
decimals = ETHEREUM_GAS_DECIMALS,
|
||||
decimals = GAS_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_gas_price),
|
||||
footer = resourceReference(R.string.send_gas_price_footer),
|
||||
|
|
@ -64,7 +64,7 @@ internal class EthereumCustomFeeConverter(
|
|||
SendTextField.CustomFee(
|
||||
value = value.gasLimit.toString(),
|
||||
decimals = GAS_DECIMALS,
|
||||
symbol = null,
|
||||
symbol = "",
|
||||
title = resourceReference(R.string.send_gas_limit),
|
||||
footer = resourceReference(R.string.send_gas_limit_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(GAS_LIMIT, it) },
|
||||
|
|
@ -95,57 +95,10 @@ internal class EthereumCustomFeeConverter(
|
|||
): ImmutableList<SendTextField.CustomFee> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
when (index) {
|
||||
FEE_AMOUNT -> {
|
||||
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals)
|
||||
val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP)
|
||||
set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS)))
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
label = getFeeFormatted(newFeeAmountDecimal),
|
||||
),
|
||||
)
|
||||
}
|
||||
GAS_PRICE -> {
|
||||
val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals)
|
||||
val newFeeAmount = gasLimit * newGasPrice
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFeeFormatted(newFeeAmount),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
else -> {
|
||||
val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newGasLimit * gasPrice
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFeeFormatted(newFeeAmount),
|
||||
),
|
||||
)
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (!checkExceedBalance(newFeeAmount)) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
FEE_AMOUNT -> setOnAmountChange(value, index)
|
||||
GAS_PRICE -> setOnGasPriceChange(value, index)
|
||||
else -> setOnGasLimitChange(value, index)
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
|
@ -170,9 +123,81 @@ internal class EthereumCustomFeeConverter(
|
|||
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setEmpty(index: Int) {
|
||||
set(index, this[index].copy(value = ""))
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnAmountChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals)
|
||||
val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP)
|
||||
set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS)))
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
label = getFeeFormatted(newFeeAmountDecimal),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnGasPriceChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals)
|
||||
val newFeeAmount = (gasLimit * newGasPrice).movePointLeft(this[FEE_AMOUNT].decimals)
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFeeFormatted(newFeeAmount),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnGasLimitChange(value: String, index: Int) {
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_LIMIT)
|
||||
} else {
|
||||
val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newGasLimit * gasPrice
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFeeFormatted(newFeeAmount),
|
||||
),
|
||||
)
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (!checkExceedBalance(newFeeAmount)) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ETHEREUM_GAS_UNIT = "GWEI"
|
||||
private const val ETHEREUM_GAS_DECIMALS = 18
|
||||
private const val FEE_AMOUNT = 0
|
||||
private const val GAS_PRICE = 1
|
||||
private const val GAS_LIMIT = 2
|
||||
|
|
|
|||
|
|
@ -75,12 +75,16 @@ internal class SendAmountFieldChangeConverter(
|
|||
}
|
||||
|
||||
private fun SendUiState.emptyState(): SendUiState {
|
||||
if (amountState == null) return this
|
||||
val amountTextField = amountState.amountTextField
|
||||
return copy(
|
||||
amountState = amountState?.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
amountTextField = amountState.amountTextField.copy(
|
||||
amountTextField = amountTextField.copy(
|
||||
value = "",
|
||||
fiatValue = "",
|
||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
|
||||
isError = false,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ internal object AmountStatePreviewData {
|
|||
segmentedButtonConfig = persistentListOf(),
|
||||
notifications = persistentListOf(),
|
||||
isFeeLoading = false,
|
||||
appCurrencyCode = "usd",
|
||||
amountTextField = SendTextField.AmountField(
|
||||
value = "123.123123123123123123",
|
||||
onValueChange = {},
|
||||
|
|
|
|||
|
|
@ -3,42 +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.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment.Companion.BottomCenter
|
||||
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 androidx.compose.ui.text.style.TextDirection
|
||||
import com.tangem.core.ui.components.fields.AmountTextField
|
||||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
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.defaultFormat
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.coroutines.job
|
||||
|
||||
@Composable
|
||||
internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean) {
|
||||
internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean, appCurrencyCode: String) {
|
||||
val decimalFormat = rememberDecimalFormat()
|
||||
val (primaryValue, secondaryValue) = if (sendField.isFiatValue) {
|
||||
sendField.fiatValue to sendField.value
|
||||
val isFiatValue = sendField.isFiatValue
|
||||
val currencyCode = if (isFiatValue) appCurrencyCode else null
|
||||
val (primaryAmount, primaryValue) = if (isFiatValue) {
|
||||
sendField.fiatAmount to sendField.fiatValue
|
||||
} else {
|
||||
sendField.value to sendField.fiatValue
|
||||
sendField.cryptoAmount to sendField.value
|
||||
}
|
||||
|
||||
val (primaryAmount, secondaryAmount) = if (sendField.isFiatValue) {
|
||||
sendField.fiatAmount to sendField.cryptoAmount
|
||||
} else {
|
||||
sendField.cryptoAmount to sendField.fiatAmount
|
||||
}
|
||||
|
||||
val requester = remember { FocusRequester() }
|
||||
AmountTextField(
|
||||
value = primaryValue,
|
||||
decimals = primaryAmount.decimals,
|
||||
symbol = primaryAmount.currencySymbol,
|
||||
visualTransformation = AmountVisualTransformation(
|
||||
decimals = primaryAmount.decimals,
|
||||
symbol = primaryAmount.currencySymbol,
|
||||
currencyCode = currencyCode,
|
||||
decimalFormat = decimalFormat,
|
||||
),
|
||||
onValueChange = sendField.onValueChange,
|
||||
keyboardOptions = sendField.keyboardOptions,
|
||||
keyboardActions = sendField.keyboardActions,
|
||||
|
|
@ -48,8 +55,8 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
|||
),
|
||||
isEnabled = isEnabled,
|
||||
isAutoResize = true,
|
||||
placeholderAlignment = TopCenter,
|
||||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing24,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -57,7 +64,18 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
|||
)
|
||||
.requiredHeightIn(min = TangemTheme.dimens.size32),
|
||||
)
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
this.coroutineContext.job.invokeOnCompletion {
|
||||
requester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
AmountSecondary(sendField, appCurrencyCode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AmountSecondary(sendField: SendTextField.AmountField, appCurrencyCode: String) {
|
||||
val secondaryAmount = if (sendField.isFiatValue) sendField.cryptoAmount else sendField.fiatAmount
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
|
|
@ -66,10 +84,18 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
|||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
val text = if (sendField.isFiatUnavailable) {
|
||||
BigDecimalFormatter.EMPTY_BALANCE_SIGN
|
||||
val text = if (sendField.isFiatValue) {
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = secondaryAmount.value,
|
||||
cryptoCurrency = secondaryAmount.currencySymbol,
|
||||
decimals = secondaryAmount.decimals,
|
||||
)
|
||||
} else {
|
||||
"${secondaryValue.ifEmpty { decimalFormat.defaultFormat() }} ${secondaryAmount.currencySymbol}"
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = secondaryAmount.value,
|
||||
fiatCurrencySymbol = secondaryAmount.currencySymbol,
|
||||
fiatCurrencyCode = appCurrencyCode,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ internal fun LazyListScope.amountField(
|
|||
AmountField(
|
||||
sendField = amountState.amountTextField,
|
||||
isEnabled = !amountState.isFeeLoading,
|
||||
appCurrencyCode = amountState.appCurrencyCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import com.tangem.core.ui.components.ResizableText
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData
|
||||
|
||||
|
|
@ -29,8 +30,16 @@ internal fun AmountBlock(
|
|||
) {
|
||||
val amount = amountState.amountTextField
|
||||
|
||||
val cryptoAmount = getAmountWithSymbol(amount.value, amount.cryptoAmount.currencySymbol)
|
||||
val fiatAmount = getAmountWithSymbol(amount.fiatValue, amount.fiatAmount.currencySymbol)
|
||||
val cryptoAmount = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = amount.cryptoAmount.value,
|
||||
cryptoCurrency = amount.cryptoAmount.currencySymbol,
|
||||
decimals = amount.cryptoAmount.decimals,
|
||||
)
|
||||
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = amount.fiatAmount.value,
|
||||
fiatCurrencySymbol = amount.fiatAmount.currencySymbol,
|
||||
fiatCurrencyCode = amountState.appCurrencyCode,
|
||||
)
|
||||
val backgroundColor = if (isEditingDisabled) {
|
||||
TangemTheme.colors.button.disabled
|
||||
} else {
|
||||
|
|
@ -77,10 +86,6 @@ internal fun AmountBlock(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getAmountWithSymbol(amount: String, symbol: String): String {
|
||||
return "$amount $symbol"
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview
|
||||
@Composable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue