Updated on 2026-08-14
This commit is contained in:
commit
36154ce4ec
10 changed files with 160 additions and 30 deletions
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue