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 c219a29944..8669d31ece 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
@@ -716,7 +716,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)
}
}
@@ -796,7 +803,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 3d230f08a8..a13a37110c 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
@@ -24,6 +25,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
@@ -175,6 +177,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(),
@@ -293,6 +296,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,
@@ -507,16 +613,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(
@@ -735,11 +831,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,
)
}
}
@@ -802,10 +898,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
@@ -831,10 +926,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
@@ -895,6 +989,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
@@ -902,5 +1004,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 9c25f78b14..d1dcdef1c0 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
@@ -286,9 +286,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,
+ )
}
}
}