Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-05 17:49:05 +03:00
parent 54f13f1082
commit d5d03d73f2
12 changed files with 289 additions and 149 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
@ -110,17 +111,21 @@ private fun ProviderContentState(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
) {
Row {
Text(
text = state.name,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.type,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
AnimatedContent(targetState = state.name, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
)
}
AnimatedContent(targetState = state.type, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
when (state.additionalBadge) {
ProviderState.AdditionalBadge.BestTrade ->
BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4))
@ -137,23 +142,27 @@ private fun ProviderContentState(
end = TangemTheme.dimens.spacing56,
),
) {
Text(
text = state.subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
if (state.percentLowerThenBest != null) {
AnimatedContent(targetState = state.subtitle, label = "") {
Text(
text = "-${state.percentLowerThenBest}%",
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.warning,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
if (state.percentLowerThenBest != null) {
AnimatedContent(targetState = state.percentLowerThenBest, label = "") {
Text(
text = "-$it%",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.warning,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
}
}
@ -198,24 +207,30 @@ private fun ProviderUnavailableState(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
) {
Row {
AnimatedContent(targetState = state.name, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
AnimatedContent(targetState = state.type, label = "") {
Text(
text = it,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
)
}
}
AnimatedContent(targetState = state.alertText, label = "") {
Text(
text = state.name,
style = TangemTheme.typography.caption2,
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = state.type,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
Text(
text = state.alertText.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}

View file

@ -14,9 +14,7 @@ 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.ExchangeProviderType
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.domain.*
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
@ -211,41 +209,7 @@ internal class StateBuilder(
): SwapStateHolder {
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val warnings = mutableListOf<SwapWarning>()
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
) {
warnings.add(
SwapWarning.PermissionNeeded(
createPermissionNotificationConfig(fromToken.symbol),
),
)
}
if (!quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.preparedSwapConfigState.isBalanceEnough
) {
warnings.add(
SwapWarning.UnableToCoverFeeWarning(
createUnableToCoverFeeNotificationConfig(
fromToken = fromToken,
onBuyClick = actions.onBuyClick,
),
),
)
}
if (!quoteModel.preparedSwapConfigState.isBalanceEnough) {
warnings.add(SwapWarning.InsufficientFunds)
}
if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) {
warnings.add(
SwapWarning.HighPriceImpact(
priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(),
notificationConfig = highPriceImpactNotificationConfig(),
),
)
}
val warnings = getWarningsForSuccessState(quoteModel, fromToken)
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
@ -288,9 +252,7 @@ internal class StateBuilder(
),
fee = feeState,
swapButton = SwapButton(
enabled = quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.preparedSwapConfigState.isFeeEnough,
enabled = getSwapButtonEnabled(quoteModel.preparedSwapConfigState),
loading = false,
onClick = actions.onSwapClick,
),
@ -306,6 +268,71 @@ internal class StateBuilder(
)
}
private fun getWarningsForSuccessState(
quoteModel: SwapState.QuotesLoadedState,
fromToken: CryptoCurrency,
): List<SwapWarning> {
val warnings = mutableListOf<SwapWarning>()
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
) {
warnings.add(
SwapWarning.PermissionNeeded(
createPermissionNotificationConfig(fromToken.symbol),
),
)
}
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
is IncludeFeeInAmount.Included ->
warnings.add(
SwapWarning.GeneralWarning(
createNetworkFeeCoverageNotificationConfig(),
),
)
else -> Unit
}
if (!quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.preparedSwapConfigState.isBalanceEnough
) {
warnings.add(
SwapWarning.UnableToCoverFeeWarning(
createUnableToCoverFeeNotificationConfig(
fromToken = fromToken,
onBuyClick = actions.onBuyClick,
),
),
)
}
// check isBalanceEnough, but for dex includeFeeInAmount always Excluded
if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
) {
warnings.add(SwapWarning.InsufficientFunds)
}
if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) {
warnings.add(
SwapWarning.HighPriceImpact(
priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(),
notificationConfig = highPriceImpactNotificationConfig(),
),
)
}
return warnings
}
private fun getSwapButtonEnabled(preparedSwapConfigState: PreparedSwapConfigState): Boolean {
return when (preparedSwapConfigState.includeFeeInAmount) {
IncludeFeeInAmount.BalanceNotEnough -> false
IncludeFeeInAmount.Excluded ->
preparedSwapConfigState.isAllowedToSpend &&
preparedSwapConfigState.isBalanceEnough &&
preparedSwapConfigState.isFeeEnough
is IncludeFeeInAmount.Included -> true
}
}
fun createQuotesErrorState(
uiStateHolder: SwapStateHolder,
swapProvider: SwapProvider,
@ -406,7 +433,7 @@ internal class StateBuilder(
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,
iconResId = R.drawable.img_attention_20,
),
)
}
@ -900,6 +927,14 @@ internal class StateBuilder(
),
)
}
private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig {
return NotificationConfig(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(R.string.send_network_fee_warning_content),
iconResId = R.drawable.img_attention_20,
)
}
// end region
private fun getShortAddressValue(fullAddress: String): String {

View file

@ -74,7 +74,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings)
if (state.permissionState is SwapPermissionState.InProgress) {
AnimatedVisibility(visible = state.permissionState is SwapPermissionState.InProgress) {
CardWithIcon(
title = stringResource(id = R.string.swapping_pending_transaction_title),
description = stringResource(id = R.string.swapping_pending_transaction_subtitle),
@ -236,6 +236,7 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
}
}
@Suppress("LongMethod")
@Composable
private fun SwapWarnings(warnings: List<SwapWarning>) {
Column(
@ -289,7 +290,6 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
is SwapWarning.GeneralWarning -> {
Notification(
config = warning.notificationConfig,
iconTint = TangemTheme.colors.icon.warning,
)
}
else -> {}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.ui
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -194,14 +195,16 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
)
SpacerW16()
if (balance.isNotBlank()) {
Text(
text = balance,
color = TangemTheme.colors.text.tertiary,
style = MaterialTheme.typography.body2,
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size20)
.padding(top = TangemTheme.dimens.spacing2),
)
AnimatedContent(targetState = balance, label = "") {
Text(
text = it,
color = TangemTheme.colors.text.tertiary,
style = MaterialTheme.typography.body2,
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size20)
.padding(top = TangemTheme.dimens.spacing2),
)
}
} else {
RectangleShimmer(
modifier = Modifier
@ -259,12 +262,14 @@ private fun Content(
}
}
is TransactionCardType.SendCard -> {
AutoSizeTextField(
modifier = sumTextModifier,
textFieldValue = textFieldValue ?: TextFieldValue(),
onAmountChange = { type.onAmountChanged(it) },
onFocusChange = type.onFocusChanged,
)
AnimatedContent(targetState = textFieldValue, label = "") {
AutoSizeTextField(
modifier = sumTextModifier,
textFieldValue = it ?: TextFieldValue(),
onAmountChange = { type.onAmountChanged(it) },
onFocusChange = type.onFocusChanged,
)
}
}
}
@ -292,12 +297,14 @@ private fun Content(
)
}
} else {
Text(
text = amountEquivalent,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
)
AnimatedContent(targetState = amountEquivalent, label = "") {
Text(
text = it,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
)
}
}
} else {
RectangleShimmer(

View file

@ -381,15 +381,22 @@ internal class SwapViewModel @Inject constructor(
private fun onSwapClick() {
singleTaskScheduler.cancelTask()
uiState = stateBuilder.createSwapInProgressState(uiState)
val provider = requireNotNull(dataState.selectedProvider) { "Selected provider is null" }
val lastLoadedQuotesState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState
if (lastLoadedQuotesState == null) {
Timber.e("Last loaded quotes state is null")
return
}
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
swapInteractor.onSwap(
swapProvider = requireNotNull(dataState.selectedProvider),
swapProvider = provider,
networkId = dataState.networkId,
swapData = dataState.swapDataModel,
currencyToSend = requireNotNull(dataState.fromCryptoCurrency),
currencyToGet = requireNotNull(dataState.toCryptoCurrency),
amountToSwap = requireNotNull(dataState.amount),
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
fee = requireNotNull(dataState.selectedFee),
)
}