Updated on 2026-08-14
This commit is contained in:
commit
720f420b61
10 changed files with 82 additions and 72 deletions
|
|
@ -28,6 +28,7 @@ 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.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
|
||||
/**
|
||||
|
|
@ -123,32 +124,49 @@ fun AmountTextField(
|
|||
private fun prepareEnter(oldValue: String, newValue: String, decimalFormat: DecimalFormat, decimals: Int): String {
|
||||
val decimalSymbol = decimalFormat.decimalFormatSymbols.decimalSeparator
|
||||
return if (decimalFormat.isValidSymbols(newValue)) {
|
||||
val parsedValue = newValue.parseBigDecimalOrNull()?.toPlainString()
|
||||
?: if (newValue.isBlank()) "" else oldValue
|
||||
val replacedWithSymbol = if (parsedValue.findLast { it != decimalSymbol } != null) {
|
||||
when {
|
||||
parsedValue.findLast { it == COMMA_SEPARATOR } != null -> {
|
||||
parsedValue.replace(COMMA_SEPARATOR, decimalSymbol)
|
||||
}
|
||||
parsedValue.findLast { it == POINT_SEPARATOR } != null -> {
|
||||
parsedValue.replace(POINT_SEPARATOR, decimalSymbol)
|
||||
}
|
||||
else -> parsedValue
|
||||
}
|
||||
} else {
|
||||
parsedValue
|
||||
}
|
||||
val joinedSymbol = if (newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)) {
|
||||
replacedWithSymbol.plus(decimalSymbol)
|
||||
} else {
|
||||
replacedWithSymbol
|
||||
}
|
||||
decimalFormat.getValidatedNumberWithFixedDecimals(joinedSymbol, decimals)
|
||||
val parsedDecimal = newValue.parseBigDecimalOrNull()
|
||||
val parsedValue = parsedDecimal?.toPlainString() ?: if (newValue.isBlank()) "" else oldValue
|
||||
|
||||
val replacedWithSymbol = parsedValue.replaceDecimalSymbol(decimalSymbol)
|
||||
val joinedSymbol = replacedWithSymbol.preserveDecimalSymbol(newValue, decimalSymbol)
|
||||
val withPreservedZeros = joinedSymbol.preserveTrailingZeros(newValue, parsedDecimal, decimalSymbol)
|
||||
decimalFormat.getValidatedNumberWithFixedDecimals(withPreservedZeros, decimals)
|
||||
} else {
|
||||
oldValue
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.replaceDecimalSymbol(decimalSymbol: Char) = if (this.findLast { it != decimalSymbol } != null) {
|
||||
when {
|
||||
this.findLast { it == COMMA_SEPARATOR } != null -> {
|
||||
this.replace(COMMA_SEPARATOR, decimalSymbol)
|
||||
}
|
||||
this.findLast { it == POINT_SEPARATOR } != null -> {
|
||||
this.replace(POINT_SEPARATOR, decimalSymbol)
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
private fun String.preserveDecimalSymbol(newValue: String, decimalSymbol: Char) = if (
|
||||
newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)
|
||||
) {
|
||||
this.plus(decimalSymbol)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
private fun String.preserveTrailingZeros(newValue: String, parsedDecimal: BigDecimal?, decimalSymbol: Char): String {
|
||||
val trailingZeros = newValue.split(decimalSymbol).getOrNull(1)?.takeLastWhile { it == '0' }.orEmpty()
|
||||
return if (parsedDecimal?.scale() == 0 && trailingZeros.isNotEmpty()) {
|
||||
"$this$decimalSymbol$trailingZeros"
|
||||
} else {
|
||||
this.plus(trailingZeros)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
|
||||
return checkDecimalSeparatorDuplicate(text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,17 @@ class GetNetworkAddressesUseCase(
|
|||
internal val networksRepository: NetworksRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId, network: Network): Flow<String> =
|
||||
operator fun invoke(userWalletId: UserWalletId, network: Network): Flow<List<String>> =
|
||||
networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network))
|
||||
.map { networkStatuses ->
|
||||
when (val networkStatus = networkStatuses.singleOrNull { it.network.id == network.id }?.value) {
|
||||
is NetworkStatus.NoAccount -> networkStatus.address.defaultAddress.value
|
||||
is NetworkStatus.Unreachable -> networkStatus.address?.defaultAddress?.value.orEmpty()
|
||||
is NetworkStatus.Verified -> networkStatus.address.defaultAddress.value
|
||||
else -> ""
|
||||
}
|
||||
networkStatuses.filter { it.network.id == network.id }
|
||||
.map { networkStatus ->
|
||||
when (val status = networkStatus.value) {
|
||||
is NetworkStatus.NoAccount -> status.address.defaultAddress.value
|
||||
is NetworkStatus.Unreachable -> status.address?.defaultAddress?.value.orEmpty()
|
||||
is NetworkStatus.Verified -> status.address.defaultAddress.value
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -296,6 +296,7 @@ internal class SendStateFactory(
|
|||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = state.sendState?.reduceAmountBy,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -331,6 +332,12 @@ internal class SendStateFactory(
|
|||
fun getSendNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
val reducedBy = sendState.reduceAmountBy.takeIf {
|
||||
notifications.none {
|
||||
it is SendNotification.Error.ExistentialDeposit ||
|
||||
it is SendNotification.Error.TransactionLimitError
|
||||
}
|
||||
}
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
isPrimaryButtonEnabled = isPrimaryButtonEnabled(
|
||||
|
|
@ -338,6 +345,7 @@ internal class SendStateFactory(
|
|||
isSending = sendState.isSending,
|
||||
notifications = notifications,
|
||||
),
|
||||
reduceAmountBy = reducedBy,
|
||||
notifications = notifications,
|
||||
showTapHelp = sendState.showTapHelp && notifications.isEmpty(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -142,7 +142,6 @@ internal sealed class SendStates {
|
|||
val txUrl: String,
|
||||
val ignoreAmountReduce: Boolean,
|
||||
val reduceAmountBy: BigDecimal?,
|
||||
val reduceAmountTo: BigDecimal?,
|
||||
val isFromConfirmation: Boolean,
|
||||
val showTapHelp: Boolean,
|
||||
val notifications: ImmutableList<SendNotification>,
|
||||
|
|
|
|||
|
|
@ -38,9 +38,6 @@ internal class SendAmountReduceToConverter(
|
|||
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isZero()
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
sendState = state.sendState?.copy(
|
||||
reduceAmountBy = value,
|
||||
),
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
|
||||
amountTextField = amountTextField.copy(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ internal class SendConfirmStateConverter(
|
|||
txUrl = "",
|
||||
ignoreAmountReduce = false,
|
||||
reduceAmountBy = null,
|
||||
reduceAmountTo = null,
|
||||
isFromConfirmation = true,
|
||||
showTapHelp = isTapHelpPreviewEnabledProvider(),
|
||||
notifications = persistentListOf(),
|
||||
|
|
|
|||
|
|
@ -60,18 +60,20 @@ internal class SendNotificationFactory(
|
|||
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value ?: BigDecimal.ZERO
|
||||
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val reduceAmountBy = sendState.reduceAmountBy ?: BigDecimal.ZERO
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
isSubtractAvailable = isSubtractAvailableProvider(),
|
||||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val sendingAmount = checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isFeeCoverage,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = sendState.reduceAmountBy,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
buildList {
|
||||
// errors
|
||||
|
|
@ -211,7 +213,7 @@ internal class SendNotificationFactory(
|
|||
),
|
||||
onConfirmClick = {
|
||||
clickIntents.onAmountReduceClick(
|
||||
reduceAmountBy = currencyDeposit,
|
||||
reduceAmountBy = currencyDeposit.minus(diff),
|
||||
clazz = SendNotification.Error.ExistentialDeposit::class.java,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal fun checkAndCalculateSubtractedAmount(
|
|||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal?,
|
||||
reduceAmountBy: BigDecimal,
|
||||
): BigDecimal {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
|
|
@ -24,17 +24,12 @@ internal fun checkAndCalculateSubtractedAmount(
|
|||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val subtractedAmount = calculateSubtractedAmount(
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
)
|
||||
return if (reduceAmountBy != null) {
|
||||
subtractedAmount.minus(reduceAmountBy)
|
||||
return if (isFeeCoverage) {
|
||||
balance.minus(reduceAmountBy).minus(feeValue)
|
||||
} else {
|
||||
subtractedAmount
|
||||
amountValue.minus(reduceAmountBy)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,26 +41,11 @@ internal fun checkFeeCoverage(
|
|||
balance: BigDecimal,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal?,
|
||||
): Boolean {
|
||||
if (!isSubtractAvailable) return false
|
||||
return balance < amountValue + feeValue && balance > feeValue && balance >= amountValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates subtracted amount
|
||||
*/
|
||||
private fun calculateSubtractedAmount(
|
||||
isFeeCoverage: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
): BigDecimal {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||
return if (isFeeCoverage) {
|
||||
minOf(amountValue, balance.minus(feeValue))
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
val amountWithReduced = (reduceAmountBy ?: BigDecimal.ZERO) + amountValue
|
||||
return balance < amountWithReduced + feeValue && balance > feeValue && balance >= amountWithReduced
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyLis
|
|||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
internal class SendRecipientWalletListConverter :
|
||||
Converter<List<AvailableWallet?>, PersistentList<SendRecipientListContent>> {
|
||||
|
|
@ -21,6 +22,7 @@ internal class SendRecipientWalletListConverter :
|
|||
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
|
||||
var walletsCounter = 0
|
||||
return this.filterNotNull()
|
||||
.filter { it.address.isNotBlank() }
|
||||
.groupBy { item -> item.name }
|
||||
.values.map {
|
||||
it.mapIndexed { index, item ->
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}.onSuccess { result ->
|
||||
combine(*result.toTypedArray()) { it }
|
||||
.onEach { wallets ->
|
||||
userWallets = wallets.filter { it.address.isNotBlank() }.toList()
|
||||
userWallets = wallets.flatMap { it }.toList()
|
||||
uiState = stateFactory.onLoadedWalletsList(wallets = userWallets)
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
|
|
@ -421,7 +421,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun List<UserWallet>.toAvailableWallets(): List<Flow<AvailableWallet>> =
|
||||
private suspend fun List<UserWallet>.toAvailableWallets(): List<Flow<List<AvailableWallet>>> =
|
||||
filterNot { it.walletId == userWalletId || it.isLocked }
|
||||
.mapNotNull { wallet ->
|
||||
val status = if (!wallet.isMultiCurrency) {
|
||||
|
|
@ -435,12 +435,14 @@ internal class SendViewModel @Inject constructor(
|
|||
} else {
|
||||
getNetworkAddressesUseCase(wallet.walletId, cryptoCurrency.network)
|
||||
}
|
||||
status?.map { address ->
|
||||
AvailableWallet(
|
||||
name = wallet.name,
|
||||
address = address,
|
||||
userWalletId = wallet.walletId,
|
||||
)
|
||||
status?.map { addresses ->
|
||||
addresses.map { address ->
|
||||
AvailableWallet(
|
||||
name = wallet.name,
|
||||
address = address,
|
||||
userWalletId = wallet.walletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -823,7 +825,7 @@ internal class SendViewModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = uiState.sendState?.reduceAmountBy,
|
||||
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue