From 62f7c337ff8cabb9b0e0384b1ed87d1c1dc63f71 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 18:45:53 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../models/response/ExpressErrorResponse.kt | 2 +- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../swap/converters/ErrorsDataConverter.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 42 +++-- .../swap/domain/models/ui/SwapState.kt | 5 +- .../feature/swap/models/SwapStateHolder.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 146 +++++++++++++++--- .../feature/swap/ui/SwapSelectTokenScreen.kt | 4 +- .../feature/swap/viewmodels/SwapViewModel.kt | 10 +- 10 files changed, 174 insertions(+), 42 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt index c0c39b0fb2..ea8a552360 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -21,7 +21,7 @@ data class ExpressError( data class ExpressErrorValue( @Json(name = "minAmount") - val minAmount: BigDecimal?, + val minAmount: String?, @Json(name = "decimals") val decimals: Int?, diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 194da3dfb2..8805ebe16d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -201,6 +201,7 @@ Операция не выполнена провайдером Посетите сайт провайдера для проверки Провайдер: требуется верификация + Список токенов в вашем кошельке Получение наилучших курсов... Провайдер Лучший курс diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8d4ff0b882..be887dc4ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -215,6 +215,7 @@ Exchange status Verified Verification required + List of all tokens added to your wallet Fetching best rates... Floating rate Go to provider diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index 5330219b1e..2068fc982a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -3,7 +3,7 @@ package com.tangem.feature.swap.converters import com.squareup.moshi.JsonAdapter import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.feature.swap.domain.models.DataError -import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.utils.converter.Converter internal class ErrorsDataConverter( @@ -23,7 +23,7 @@ internal class ErrorsDataConverter( 2240 -> DataError.ExchangeNotPossibleError(code = error.code) 2250 -> DataError.ExchangeTooSmallAmountError( code = error.code, - amount = SwapAmount( + amount = createFromAmountWithOffset( requireNotNull(error.value?.minAmount), requireNotNull(error.value?.decimals), ), diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index da5dd7b595..afc2b3787b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -501,14 +501,16 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network, ) - return result.fold(ifLeft = { - when (it) { - is SendTransactionError.NetworkError -> TxState.NetworkError - is SendTransactionError.DataError -> TxState.BlockchainError - SendTransactionError.DemoCardError -> TxState.UnknownError - else -> TxState.UnknownError - } - }, ifRight = { + return result.fold( + ifLeft = { + when (it) { + is SendTransactionError.NetworkError -> TxState.NetworkError + is SendTransactionError.DataError -> TxState.BlockchainError + SendTransactionError.DemoCardError -> TxState.UnknownError + else -> TxState.UnknownError + } + }, + ifRight = { TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -523,7 +525,8 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath, ) ?: "", ) - },) + }, + ) } @Deprecated("used in old swap mechanism") @@ -707,7 +710,14 @@ internal class SwapInteractorImpl @Inject constructor( } } } else { - return SwapState.SwapError(quoteDataModel.error) + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError(fromTokenSwapInfo, quoteDataModel.error) } } @@ -787,7 +797,17 @@ internal class SwapInteractorImpl @Inject constructor( ), ) } else { - return SwapState.SwapError(it.error) + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError( + fromTokenSwapInfo, + it.error, + ) } } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 0c98986dc1..b49765d79d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -31,7 +31,10 @@ sealed interface SwapState { val zeroAmountEquivalent: String, ) : SwapState - data class SwapError(val error: DataError) : SwapState + data class SwapError( + val fromTokenInfo: TokenSwapInfo, + val error: DataError, + ) : SwapState } sealed class PermissionDataState { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 37ef409ce5..9fc0d8a9c1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -123,6 +123,7 @@ sealed interface SwapWarning { data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning + data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { 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 7d965aa10f..4a90e77fab 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 @@ -13,6 +13,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation @@ -25,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode +import kotlin.math.min /** * State builder creates a specific states for SwapScreen @@ -176,6 +178,7 @@ internal class StateBuilder( balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), ), + warnings = emptyList(), fee = FeeItemState.Empty, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), providerState = ProviderState.Loading(), @@ -294,6 +297,109 @@ internal class StateBuilder( ) } + fun createQuotesErrorState( + uiStateHolder: SwapStateHolder, + swapProvider: SwapProvider, + fromToken: TokenSwapInfo, + toToken: CryptoCurrencyStatus?, + dataError: DataError, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder + val warning = getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency) + val providerState = getProviderStateForError( + swapProvider = swapProvider, + fromToken = fromToken.cryptoCurrencyStatus.currency, + dataError = dataError, + selectionType = ProviderState.SelectionType.CLICK, + ) + val receiveCardData = toToken?.let { + SwapCardState.SwapCardData( + type = TransactionCardType.ReceiveCard(), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = toToken, + tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, + coinId = toToken.currency.network.backendId, + isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, + tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, + canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, + balance = toToken.getFormattedAmount(), + isBalanceHidden = isBalanceHiddenProvider(), + ) + } ?: SwapCardState.Empty( + type = TransactionCardType.ReceiveCard(), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountTextFieldValue = TextFieldValue( + text = "0", + ), + canSelectAnotherToken = true, + ) + return uiStateHolder.copy( + sendCardData = uiStateHolder.sendCardData.copy( + amountEquivalent = getFormattedFiatAmount(fromToken.amountFiat), + ), + receiveCardData = receiveCardData, + warnings = listOf(warning), + permissionState = SwapPermissionState.Empty, + fee = FeeItemState.Empty, + swapButton = SwapButton( + enabled = false, + loading = false, + onClick = actions.onSwapClick, + ), + updateInProgress = false, + providerState = providerState, + ) + } + + private fun getProviderStateForError( + swapProvider: SwapProvider, + fromToken: CryptoCurrency, + dataError: DataError, + selectionType: ProviderState.SelectionType, + ): ProviderState { + return when (dataError) { + is DataError.ExchangeTooSmallAmountError -> { + swapProvider.convertToUnavailableProviderState( + alertText = resourceReference( + R.string.express_provider_min_amount, + wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + selectionType = selectionType, + onProviderClick = actions.onProviderClick, + ) + } + else -> { + ProviderState.Empty() + } + } + } + + private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning { + return when (dataError) { + is DataError.ExchangeTooSmallAmountError -> SwapWarning.TooSmallAmountWarning( + notificationConfig = NotificationConfig( + title = resourceReference( + id = R.string.warning_express_too_minimal_amount_title, + formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + subtitle = resourceReference(R.string.warning_express_too_minimal_amount_description), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + else -> SwapWarning.GeneralWarning( + notificationConfig = NotificationConfig( + title = resourceReference(R.string.common_error), + subtitle = resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + } + } + fun createQuotesEmptyAmountState( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, @@ -473,10 +579,9 @@ internal class StateBuilder( val fee = uiState.fee as? FeeItemState.Content ?: return uiState val fromCryptoCurrencyStatus = requireNotNull(fromToken.token) val toCryptoCurrencyStatus = requireNotNull(toToken.token) - val rate = txState.toAmount?.toBigDecimal()?.divide( - txState.fromAmount?.toBigDecimal(), + val rate = txState.toAmount?.toBigDecimal()?.calculateRate( + txState.fromAmount?.toBigDecimal() ?: BigDecimal.ZERO, toCryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromCryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toCryptoCurrencyStatus.currency.symbol @@ -510,16 +615,6 @@ internal class StateBuilder( ) } - fun mapError(uiState: SwapStateHolder, error: DataError, onClick: () -> Unit): SwapStateHolder { - return when (error) { - // todo use if needed later - // DataError.InsufficientLiquidity -> TODO() - // DataError.NoError -> TODO() - is DataError.ExchangeTooSmallAmountError -> addWarning(uiState, error.amount.toString(), true, onClick) - else -> addWarning(uiState, null, false) {} - } - } - fun addAlert(uiState: SwapStateHolder, onClick: () -> Unit): SwapStateHolder { return uiState.copy( alert = SwapWarning.GenericWarning( @@ -738,11 +833,11 @@ internal class StateBuilder( onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) - // todo handle error - is SwapState.SwapError -> provider.convertToUnavailableProviderState( - alertText = resourceReference(R.string.express_provider_min_amount, wrappedList("10")), - selectionType = ProviderState.SelectionType.NONE, - onProviderClick = onProviderSelect, + is SwapState.SwapError -> getProviderStateForError( + swapProvider = provider, + fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency, + dataError = state.error, + selectionType = ProviderState.SelectionType.SELECT, ) } } @@ -805,10 +900,9 @@ internal class StateBuilder( selectionType: ProviderState.SelectionType, onProviderClick: (String) -> Unit, ): ProviderState { - val rate = toTokenInfo.tokenAmount.value.divide( + val rate = toTokenInfo.tokenAmount.value.calculateRate( fromTokenInfo.tokenAmount.value, toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol @@ -834,10 +928,9 @@ internal class StateBuilder( ): ProviderState { val fromTokenInfo = state.fromTokenInfo val toTokenInfo = state.toTokenInfo - val rate = toTokenInfo.tokenAmount.value.divide( + val rate = toTokenInfo.tokenAmount.value.calculateRate( fromTokenInfo.tokenAmount.value, toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol @@ -898,6 +991,14 @@ internal class StateBuilder( return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) } + private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { + return "${this.formatToUIRepresentation()} ${token.network.currencySymbol}" + } + + private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { + return this.divide(to, min(decimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) + } + private companion object { const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_FIRST_PART_LENGTH = 7 @@ -905,5 +1006,6 @@ internal class StateBuilder( private const val PRICE_IMPACT_THRESHOLD = 0.1 private const val HUNDRED_PERCENTS = 100 private const val UNKNOWN_AMOUNT_SIGN = "—" + private const val MAX_DECIMALS_TO_SHOW = 8 } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 6e89f78c3a..cde932bf0d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -43,7 +43,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = "", // todo add title + subtitle = stringResource(id = R.string.express_exchange_token_list_subtitle), ) }, ) @@ -55,7 +55,7 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = LazyColumn( modifier = modifier .background(color = screenBackgroundColor) - .fillMaxWidth(), + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { item { SpacerH8() } 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 a577a98dfe..4fb07606b9 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 @@ -288,9 +288,13 @@ internal class SwapViewModel @Inject constructor( ) } is SwapState.SwapError -> { - Timber.e("SwapError when loading quotes ${state.error}") - // todo handle when change token and error - uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } + uiState = stateBuilder.createQuotesErrorState( + uiStateHolder = uiState, + swapProvider = provider, + fromToken = state.fromTokenInfo, + toToken = dataState.toCryptoCurrency, + dataError = state.error, + ) } } }