Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-29 16:02:01 +03:00
commit 222bfd9a3a
25 changed files with 962 additions and 342 deletions

View file

@ -0,0 +1,172 @@
package com.tangem.core.ui.components.fields
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
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
import androidx.compose.ui.Alignment.Companion.TopStart
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
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,
decimals: Int,
onValueChange: (String) -> Unit,
textStyle: TextStyle,
modifier: Modifier = Modifier,
symbol: String? = null,
color: Color = TangemTheme.colors.text.primary1,
placeholderAlignment: Alignment = TopStart,
showPlaceholder: Boolean = true,
keyboardOptions: KeyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
) {
val decimalFormat = rememberDecimalFormat()
val placeholderTextAlign = if (placeholderAlignment == TopCenter) {
TextAlign.Center
} else {
TextAlign.Start
}
SimpleTextField(
value = value,
onValueChange = { newText ->
if (decimalFormat.isValidSymbols(newText)) {
val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
onValueChange(trimmed)
}
},
modifier = modifier
.background(TangemTheme.colors.background.action),
textStyle = textStyle,
color = color,
keyboardOptions = keyboardOptions,
singleLine = true,
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
decorationBox = { innerTextField ->
Box {
if (value.isBlank() && showPlaceholder) {
val placeholder = if (symbol != null) {
decimalFormat.defaultFormat().plus(" $symbol")
} else {
decimalFormat.defaultFormat()
}
Text(
text = placeholder,
style = textStyle,
color = TangemTheme.colors.text.disabled,
textAlign = placeholderTextAlign,
modifier = Modifier
.align(placeholderAlignment),
)
}
innerTextField()
}
},
)
}
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text)
}
// region preview
@Preview(locale = "en", showBackground = true, name = "English")
@Preview(locale = "ru", showBackground = true, name = "Russian")
@Composable
private fun AmountTextFieldPreview(
@PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData,
) {
var text by remember { mutableStateOf(amount.value.orEmpty()) }
TangemTheme {
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,
textAlign = TextAlign.Center,
),
modifier = Modifier.fillMaxWidth(),
)
}
}
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,
placeholderAlignment = TopStart,
),
)
}
private data class AmountTextFieldPreviewData(
val symbol: String? = "$",
val value: String? = null,
val decimals: Int = 2,
val showPlaceholder: Boolean,
val placeholderAlignment: Alignment,
)
// endregion

View file

@ -3,14 +3,18 @@ package com.tangem.core.ui.components.fields
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
@ -19,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
/**
* Simple text field with placeholder
*/
@Suppress("ReusedModifierInstance")
@Composable
fun SimpleTextField(
value: String,
@ -29,32 +34,72 @@ fun SimpleTextField(
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
color: Color = TangemTheme.colors.text.primary1,
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
readOnly: Boolean = false,
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TangemTheme.typography.body2.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
decorationBox = { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.disabled,
modifier = Modifier,
)
}
textValue()
}
},
modifier = modifier
.focusRequester(focusRequester),
var textFieldValueState by remember {
mutableStateOf(
TextFieldValue(
text = value,
selection = when {
value.isEmpty() -> TextRange.Zero
else -> TextRange(value.length, value.length)
},
),
)
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
handleColor = TangemTheme.colors.text.secondary,
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
)
val textFieldValue = textFieldValueState.copy(text = value)
SideEffect {
if (textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
) {
textFieldValueState = textFieldValue
}
}
var lastTextValue by remember(value) { mutableStateOf(value) }
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
BasicTextField(
value = textFieldValue,
onValueChange = { newTextFieldValueState ->
textFieldValueState = newTextFieldValueState
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
if (stringChangedSinceLastInvocation) {
onValueChange(newTextFieldValueState.text)
}
},
textStyle = textStyle.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
decorationBox = decorationBox ?: { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.disabled,
)
}
textValue()
}
},
modifier = modifier
.focusRequester(focusRequester),
)
}
}

View file

@ -5,28 +5,49 @@ 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.formatWithThousands
import java.text.DecimalFormat
class AmountVisualTransformation(
private val symbol: String,
private val decimals: Int,
private val symbol: String? = null,
private val decimalFormat: DecimalFormat = DecimalFormat(),
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
buildAnnotatedString {
append(text)
if (text.isNotBlank()) {
append(" ")
append(symbol)
}
},
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return text.length
}
override fun transformedToOriginal(offset: Int): Int {
return text.length
override fun filter(text: AnnotatedString): TransformedText {
val formattedText = decimalFormat.formatWithThousands(
text.text,
decimals,
)
val groupingSymbol = decimalFormat.decimalFormatSymbols.groupingSeparator
return TransformedText(
text = buildAnnotatedString {
append(formattedText)
if (formattedText.isNotEmpty() && symbol != null) {
append(" $symbol")
}
},
offsetMapping = OffsetMappingImpl(text.text, formattedText, groupingSymbol),
)
}
private class OffsetMappingImpl(
private val text: String,
private val formattedText: String,
private val gropingSymbol: Char,
) : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
var noneDigitCount = 0
var i = 0
while (i < offset + noneDigitCount) {
if (formattedText.getOrNull(i++) == gropingSymbol) noneDigitCount++
}
return (offset + noneDigitCount).coerceIn(0, formattedText.length)
}
override fun transformedToOriginal(offset: Int): Int {
val noneDigitCount = formattedText.take(offset).count { it == gropingSymbol }
return (offset - noneDigitCount).coerceIn(0, text.length)
}
}
}

View file

@ -0,0 +1,104 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Input row for entering amount. Manages correct amount format and validation
*
* @param title title reference
* @param text primary text reference
* @param onValueChange text change callback
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param keyboardOptions keyboard options for field
* @param iconRes action icon
* @param iconTint action icon tint
* @param onIconClick click on action icon
* @param showDivider show divider
*
* @see [InputRowDefault] for read only version
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&
* t=IQ5lBJEkFGU4WSvi-4>InputRowEnter</a>
*/
@Composable
fun InputRowEnterAmount(
title: TextReference,
text: String,
decimals: Int,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
symbol: String? = null,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
AmountTextField(
value = text,
decimals = decimals,
symbol = symbol,
onValueChange = onValueChange,
color = textColor,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
)
}
iconRes?.let {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },
)
}
}
}
}

View file

@ -0,0 +1,94 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* `Input Row Enter Info` for entering amount. Manages correct amount format and validation
* @param title title reference
* @param text primary text reference
* @param onValueChange text change callback
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param isSingleLine text
* @param visualTransformation applied transformation to text
* @param keyboardOptions keyboard options for field
* @param showDivider show divider
*
* @see [InputRowEnterInfo]
* @see <a href=https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode
* =design&t=IQ5lBJEkFGU4WSvi-4>Input Row Enter</a>
* @see <a href=https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node
* * -id=7854-33577&mode=design&t=6o23sqF8fDQdn4C5-4>Input Row Enter Info</a>
*/
@Composable
fun InputRowEnterInfoAmount(
title: TextReference,
text: String,
decimals: Int,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
symbol: String? = null,
info: TextReference? = null,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
infoColor: Color = TangemTheme.colors.text.tertiary,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Row {
AmountTextField(
value = text,
decimals = decimals,
symbol = symbol,
onValueChange = onValueChange,
color = textColor,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.weight(1f),
)
info?.let {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = infoColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.align(Alignment.Bottom),
)
}
}
}
}
}

View file

@ -0,0 +1,153 @@
package com.tangem.core.ui.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalConfiguration
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
private const val TEXT_CHUNK_THOUSAND = 3
private const val POINT_SEPARATOR = '.'
@Composable
fun rememberDecimalFormat(): DecimalFormat {
val locale = LocalConfiguration.current.locale
val decimalSymbols = remember { DecimalFormatSymbols.getInstance(locale) }
return remember {
DecimalFormat().apply {
decimalFormatSymbols = decimalSymbols
isParseBigDecimal = true
}
}
}
/**
* Formats input [String] for InputField, to remove wrong symbols, letters etc
* Use [decimals] for cut this number symbols after floating point
*
* Example (with 8 decimals):
* input string - ab123.46377372ab53
* result string 123.46377372
*/
fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: Int): String {
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
val decimalSeparator = decimalFormatSymbols.decimalSeparator
val lastChar = text.lastOrNull()
val trimmedText = if (text.isNotEmpty() && (lastChar == thousandsSeparator || lastChar == POINT_SEPARATOR)) {
text.dropLast(1) + decimalSeparator
} else {
text
}
if (trimmedText.startsWith("0") && trimmedText.length > 1 && trimmedText[1] != decimalSeparator) {
return "0"
}
val filteredChars = trimmedText.replace(thousandsSeparator.toString(), "").filterIndexed { index, c ->
val isOneOrZeroPoint =
c == decimalSeparator && index != 0 && trimmedText.count { it == decimalSeparator } <= 1
val isIndexPointIndex =
c == decimalSeparator && index != 0 && trimmedText.indexOf(decimalSeparator) == index
c.isDigit() || isIndexPointIndex || isOneOrZeroPoint
}
// If dot is present, take first digits before decimal and first decimals digits after decimal
return if (filteredChars.count { it == decimalSeparator } == 1) {
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
}
// If there is no dot, just take all digits
else {
filteredChars
}
}
/**
* Formats input [text] with grouping and decimal separators.
* Takes into account [decimals] number of digits after floating point.
*/
fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String {
val thousandsSeparator = decimalFormatSymbols.groupingSeparator
val decimalSeparator = decimalFormatSymbols.decimalSeparator
val localizedText = text.replace("[,.]".toRegex(), decimalSeparator.toString())
return if (localizedText.count { it == decimalSeparator } == 1) {
val beforeDecimal = localizedText.substringBefore(decimalSeparator)
.reversed()
.chunked(TEXT_CHUNK_THOUSAND)
.joinToString(thousandsSeparator.toString())
.reversed()
val afterDecimal = localizedText.substringAfter(decimalSeparator)
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
}
// If there is no dot, just take all digits
else {
localizedText.reversed()
.chunked(TEXT_CHUNK_THOUSAND)
.joinToString(thousandsSeparator.toString())
.reversed()
}
}
fun DecimalFormat.defaultFormat(): String {
return "0${decimalFormatSymbols.decimalSeparator}00"
}
/**
* Checks if text input contains extra decimal separators.
* If so, it will return false, otherwise true.
*
* Note: number can contain only one decimal separator.
*/
fun DecimalFormat.checkDecimalSeparatorDuplicate(text: String): Boolean {
val regex = "[${decimalFormatSymbols.decimalSeparator}]".toRegex()
val decimalSeparatorCount = regex.findAll(text).count()
return decimalSeparatorCount <= 1 // only one decimal separator
}
/**
* Checks if text input contains grouping separators.
* If so, it will return false, otherwise true.
*
* Note: grouping separators are used only for VisualTransformations.
*/
fun DecimalFormat.checkGroupingSeparator(text: String): Boolean {
val regex = "[${decimalFormatSymbols.groupingSeparator}]".toRegex()
val decimalSeparatorCount = regex.findAll(text).count()
return decimalSeparatorCount == 0 // no grouping separator
}
fun String.parseToBigDecimal(decimals: Int): BigDecimal {
val decimalFormat = DecimalFormat().apply {
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
isParseBigDecimal = true
maximumFractionDigits = decimals
minimumFractionDigits = decimals
}
return try {
decimalFormat.parse(this) as? BigDecimal ?: BigDecimal.ZERO
} catch (e: Exception) {
BigDecimal.ZERO
}
}
fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN): String {
val decimalFormat = DecimalFormat().apply {
decimalFormatSymbols = DecimalFormatSymbols(Locale.getDefault())
isParseBigDecimal = true
isGroupingUsed = false
maximumFractionDigits = decimals
minimumFractionDigits = 0
this.roundingMode = roundingMode
}
return try {
decimalFormat.format(this)
} catch (e: Exception) {
""
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.utils
import java.text.DecimalFormat
@Deprecated("Deprecated due to unnecessary abstraction. Use methods from DecimalFormatterExt")
class InputNumberFormatter(
numberFormat: DecimalFormat,
) {

View file

@ -0,0 +1,30 @@
package com.tangem.domain.tokens.model
import java.math.BigDecimal
data class Amount(
val currencySymbol: String,
val value: BigDecimal? = null,
val decimals: Int,
val type: AmountType = AmountType.CoinType,
)
sealed class AmountType {
object CoinType : AmountType()
object ReserveType : AmountType()
data class TokenType(val token: CryptoCurrency.Token) : AmountType()
data class FiatType(val code: String) : AmountType()
}
/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */
fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount(
currencySymbol = cryptoCurrency.symbol,
value = this,
decimals = cryptoCurrency.decimals,
type = when (cryptoCurrency) {
is CryptoCurrency.Coin -> AmountType.CoinType
is CryptoCurrency.Token -> AmountType.TokenType(
token = cryptoCurrency,
)
},
)

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

@ -0,0 +1,32 @@
package com.tangem.features.send.impl.presentation.state.amount
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 com.tangem.utils.isNullOrZero
internal class SendAmountCurrencyConverter(
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Boolean, SendUiState> {
override fun convert(value: Boolean): SendUiState {
val state = currentStateProvider()
val amountState = state.amountState ?: return state
val amountTextField = amountState.amountTextField
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
return if (amountTextField.isFiatValue == value && !isValidFiatRate) {
state
} else {
return state.copy(
amountState = amountState.copy(
amountTextField = amountTextField.copy(
isFiatValue = value,
),
),
)
}
}
}

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
}