Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-29 19:39:35 +03:00
commit 36154ce4ec
10 changed files with 160 additions and 30 deletions

View file

@ -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?,

View file

@ -201,6 +201,7 @@
<string name="express_exchange_notification_failed_title">Операция не выполнена провайдером</string>
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
<string name="express_exchange_notification_verification_title">Провайдер: требуется верификация</string>
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
<string name="express_provider">Провайдер</string>
<string name="express_provider_best_rate">Лучший курс</string>

View file

@ -215,6 +215,7 @@
<string name="express_exchange_status_title">Exchange status</string>
<string name="express_exchange_status_verified">Verified</string>
<string name="express_exchange_status_verifying">Verification required</string>
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
<string name="express_fetch_best_rates">Fetching best rates...</string>
<string name="express_floating_rate">Floating rate</string>
<string name="express_go_to_provider">Go to provider</string>

View file

@ -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),
),

View file

@ -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,
)
}
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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() }

View file

@ -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,
)
}
}
}