From 0ddd171bb273117aca71c84d681891b5ad893d87 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Nov 2024 12:42:36 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 2 - .../bigdecimal/BigDecimalCryptoFormat.kt | 37 ++++-- .../tangem/data/tokens/di/TokensDataModule.kt | 10 +- .../DefaultCurrencyChecksRepository.kt | 15 ++- .../GetMinimumTransactionAmountSyncUseCase.kt | 11 +- .../tangem/feature/swap/ui/StateBuilder.kt | 55 +++++++-- .../feature/swap/viewmodels/SwapViewModel.kt | 110 +++++++++++------- 7 files changed, 155 insertions(+), 85 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 0776b43400..7e547182ce 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -277,11 +277,9 @@ internal object TokensDomainModule { @Singleton fun provideGetMinimumTransactionAmountSyncUseCase( currencyChecksRepository: CurrencyChecksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetMinimumTransactionAmountSyncUseCase { return GetMinimumTransactionAmountSyncUseCase( currencyChecksRepository = currencyChecksRepository, - dispatchers = dispatchers, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index eea3130209..7d6849591c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.FORMAT_THRESHOLD import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE import com.tangem.utils.extensions.isNotWhitespace import java.math.BigDecimal import java.math.RoundingMode @@ -16,6 +17,7 @@ open class BigDecimalCryptoFormat( val symbol: String, val decimals: Int, val locale: Locale = Locale.getDefault(), + val ignoreSymbolPosition: Boolean = false, ) : BigDecimalFormat { override fun invoke(value: BigDecimal): String = defaultAmount()(value) @@ -48,11 +50,13 @@ fun BigDecimalFormatScope.crypto( fun BigDecimalFormatScope.crypto( cryptoCurrency: CryptoCurrency, + ignoreSymbolPosition: Boolean = false, locale: Locale = Locale.getDefault(), ): BigDecimalCryptoFormat { return BigDecimalCryptoFormat( symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, + ignoreSymbolPosition = ignoreSymbolPosition, locale = locale, ) } @@ -60,19 +64,28 @@ fun BigDecimalFormatScope.crypto( // == Formatters == fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = usdCurrency - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP + if (ignoreSymbolPosition) { + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + formatter.format(value) + NON_BREAKING_SPACE + symbol + } else { + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) } - - formatter.format(value) - .replaceFiatSymbolWithCrypto( - fiatCurrencySymbol = usdCurrency.getSymbol(locale), - cryptoCurrencySymbol = symbol, - ) } fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 3e5c70b106..cc7bb7a3fe 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -95,8 +95,14 @@ internal object TokensDataModule { @Provides @Singleton - fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository { - return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade) + fun provideCurrencyChecksRepository( + walletManagersFacade: WalletManagersFacade, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): CurrencyChecksRepository { + return DefaultCurrencyChecksRepository( + walletManagersFacade = walletManagersFacade, + coroutineDispatchers = coroutineDispatcherProvider, + ) } @Provides diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index b156d85caf..a62d70a3f8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -12,10 +12,13 @@ import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext import java.math.BigDecimal internal class DefaultCurrencyChecksRepository( private val walletManagersFacade: WalletManagersFacade, + private val coroutineDispatchers: CoroutineDispatcherProvider, ) : CurrencyChecksRepository { override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { val manager = walletManagersFacade.getOrCreateWalletManager( @@ -45,12 +48,14 @@ internal class DefaultCurrencyChecksRepository( } override suspend fun getMinimumSendAmount(userWalletId: UserWalletId, network: Network): BigDecimal? { - val manager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - network = network, - ) + return withContext(coroutineDispatchers.io) { + val manager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) - return if (manager is MinimumSendAmountProvider) manager.getMinimumSendAmount() else null + if (manager is MinimumSendAmountProvider) manager.getMinimumSendAmount() else null + } } override suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt index 73aaeb8c0d..388b88d7e7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt @@ -5,22 +5,17 @@ import arrow.core.raise.either import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import java.math.BigDecimal class GetMinimumTransactionAmountSyncUseCase( private val currencyChecksRepository: CurrencyChecksRepository, - private val dispatchers: CoroutineDispatcherProvider, ) { suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Either = withContext(dispatchers.io) { - either { - val cryptoCurrency = cryptoCurrencyStatus.currency - currencyChecksRepository.getMinimumSendAmount(userWalletId, cryptoCurrency.network) - } + ): Either = either { + val cryptoCurrency = cryptoCurrencyStatus.currency + currencyChecksRepository.getMinimumSendAmount(userWalletId, cryptoCurrency.network) } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index d5f2d4d92b..e841fb0bba 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -172,10 +172,15 @@ internal class StateBuilder( val canSelectReceiveToken = mainTokenId != toToken.id.value if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( - isError = false, - header = TextReference.Res(R.string.swapping_from_title), - ) + val sendInputType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) + val sendInput = if (sendInputType.isError) { + sendInputType + } else { + sendInputType.copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) + } return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = sendInput, @@ -252,10 +257,16 @@ internal class StateBuilder( } else { TextReference.Res(R.string.swapping_from_title) } - val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( - isError = isInsufficientFunds, - header = insufficientFundsHeader, - ) + val sendCardType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) + val sendInput = if (sendCardType.isError && !isInsufficientFunds) { + // if any error in inputField and funds enough -> show that error else show fund is not enough error + sendCardType + } else { + sendCardType.copy( + isError = isInsufficientFunds, + header = insufficientFundsHeader, + ) + } return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = sendInput, @@ -863,14 +874,36 @@ internal class StateBuilder( ) } - fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder { + fun updateSwapAmount( + uiState: SwapStateHolder, + amountFormatted: String, + amountRaw: String, + fromToken: CryptoCurrency, + minTxAmount: BigDecimal?, + ): SwapStateHolder { if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState + val amountToSend = amountRaw.toBigDecimalOrNull() + val sendInput = if (minTxAmount != null && amountToSend != null && amountToSend < minTxAmount) { + val minAmountFormatted = minTxAmount.format { + crypto(cryptoCurrency = fromToken, ignoreSymbolPosition = true) + } + (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( + isError = true, + header = resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)), + ) ?: uiState.sendCardData.type + } else { + (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) ?: uiState.sendCardData.type + } return uiState.copy( sendCardData = uiState.sendCardData.copy( amountTextFieldValue = TextFieldValue( - text = amount, - selection = TextRange(amount.length), + text = amountFormatted, + selection = TextRange(amountFormatted.length), ), + type = sendInput, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index c4854ddb29..216f2860fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -83,6 +84,7 @@ internal class SwapViewModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, swapInteractorFactory: SwapInteractor.Factory, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -842,60 +844,78 @@ internal class SwapViewModel @Inject constructor( } private fun onChangeCardsClicked() { - val newFromToken = dataState.toCryptoCurrency - val newToToken = dataState.fromCryptoCurrency + viewModelScope.launch { + val newFromToken = dataState.toCryptoCurrency + val newToToken = dataState.fromCryptoCurrency - if (newFromToken != null && newToToken != null) { - isAmountChangedByUser = true + if (newFromToken != null && newToToken != null) { + isAmountChangedByUser = true - dataState = dataState.copy( - fromCryptoCurrency = newFromToken, - toCryptoCurrency = newToToken, - ) - isOrderReversed = !isOrderReversed - dataState.tokensDataState?.let { - updateTokensState(it) + dataState = dataState.copy( + fromCryptoCurrency = newFromToken, + toCryptoCurrency = newToToken, + ) + isOrderReversed = !isOrderReversed + dataState.tokensDataState?.let { + updateTokensState(it) + } + + val minTxAmount = getMinimumTransactionAmountSyncUseCase( + userWalletId, + newFromToken, + ).getOrNull() + val decimals = newFromToken.currency.decimals + lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) + uiState = stateBuilder.updateSwapAmount( + uiState = uiState, + amountFormatted = inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), + amountRaw = lastAmount.value, + fromToken = newFromToken.currency, + minTxAmount = minTxAmount, + ) + startLoadingQuotes( + fromToken = newFromToken, + toToken = newToToken, + amount = lastAmount.value, + toProvidersList = findSwapProviders(newFromToken, newToToken), + ) } - - val decimals = newFromToken.currency.decimals - lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) - uiState = stateBuilder.updateSwapAmount( - uiState, - inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), - ) - startLoadingQuotes( - fromToken = newFromToken, - toToken = newToToken, - amount = lastAmount.value, - toProvidersList = findSwapProviders(newFromToken, newToToken), - ) } } private fun onAmountChanged(value: String) { - val fromToken = dataState.fromCryptoCurrency - val toToken = dataState.toCryptoCurrency - if (fromToken != null) { - val decimals = fromToken.currency.decimals - val cutValue = cutAmountWithDecimals(decimals, value) - lastAmount.value = cutValue - uiState = stateBuilder.updateSwapAmount( - uiState = uiState, - amount = inputNumberFormatter.formatWithThousands(cutValue, decimals), - ) + viewModelScope.launch { + val fromToken = dataState.fromCryptoCurrency + val toToken = dataState.toCryptoCurrency + if (fromToken != null) { + val decimals = fromToken.currency.decimals + val cutValue = cutAmountWithDecimals(decimals, value) + val minTxAmount = getMinimumTransactionAmountSyncUseCase( + userWalletId, + fromToken, + ).getOrNull() + lastAmount.value = cutValue + uiState = stateBuilder.updateSwapAmount( + uiState = uiState, + amountFormatted = inputNumberFormatter.formatWithThousands(cutValue, decimals), + amountRaw = lastAmount.value, + fromToken = fromToken.currency, + minTxAmount = minTxAmount, + ) - if (toToken != null) { - if (toToken.value.amount != null) { - isAmountChangedByUser = true - } + if (toToken != null) { + if (toToken.value.amount != null) { + isAmountChangedByUser = true + } - amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { - startLoadingQuotes( - fromToken = fromToken, - toToken = toToken, - amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken), - ) + amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { + startLoadingQuotes( + fromToken = fromToken, + toToken = toToken, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken), + ) + } } } }