Updated on 2026-08-14
This commit is contained in:
commit
f6796c4e19
16 changed files with 165 additions and 118 deletions
|
|
@ -681,7 +681,7 @@
|
|||
<string name="warning_express_no_exchangeable_coins_title">No available tokens to swap</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">To make a transaction you need to deposit some %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Unable to cover %s fee</string>
|
||||
<string name="warning_express_refresh_required_title">Service temporary unavailable</string>
|
||||
<string name="warning_express_refresh_required_title">Service temporarily unavailable</string>
|
||||
<string name="warning_express_too_minimal_amount_description">Please change the amount to swap</string>
|
||||
<string name="warning_express_too_minimal_amount_title">The amount to swap must be at least %s</string>
|
||||
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -25,23 +22,9 @@ sealed interface Keyboard {
|
|||
/**
|
||||
* Allows to subscribe to a soft keyboard to detect when it's open/closed
|
||||
*/
|
||||
@Deprecated("Use Modifier.imePadding() on pure Compose screens (without XML layouts)")
|
||||
@Composable
|
||||
fun keyboardAsState(): State<Keyboard> {
|
||||
val density = LocalDensity.current
|
||||
val imeInsets = WindowInsets.ime
|
||||
.exclude(WindowInsets.navigationBars)
|
||||
.getBottom(density)
|
||||
|
||||
return remember(imeInsets) {
|
||||
derivedStateOf {
|
||||
if (imeInsets > 0) {
|
||||
Keyboard.Opened(
|
||||
height = with(density) { imeInsets.toDp() },
|
||||
)
|
||||
} else {
|
||||
Keyboard.Closed
|
||||
}
|
||||
}
|
||||
}
|
||||
val bottom = WindowInsets.ime.getBottom(LocalDensity.current)
|
||||
val isImeVisible = bottom > 0
|
||||
return rememberUpdatedState(if (isImeVisible) Keyboard.Opened(bottom.dp) else Keyboard.Closed)
|
||||
}
|
||||
|
|
@ -118,12 +118,12 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
|
||||
val isMulticurrencyWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
|
||||
// swap
|
||||
if (isMulticurrencyWallet &&
|
||||
marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)
|
||||
) {
|
||||
activeList.add(TokenActionsState.ActionState.Swap(true))
|
||||
} else {
|
||||
disabledList.add(TokenActionsState.ActionState.Swap(false))
|
||||
if (isMulticurrencyWallet) {
|
||||
if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)) {
|
||||
activeList.add(TokenActionsState.ActionState.Swap(true))
|
||||
} else {
|
||||
disabledList.add(TokenActionsState.ActionState.Swap(false))
|
||||
}
|
||||
}
|
||||
|
||||
// buy
|
||||
|
|
@ -164,11 +164,13 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
return activeList + disabledList
|
||||
}
|
||||
|
||||
private fun isSendDisabled(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean =
|
||||
cryptoCurrencyStatus.value.amount.isNullOrZero() ||
|
||||
coinStatus?.value?.amount.isNullOrZero() ||
|
||||
currenciesRepository.hasPendingTransactions(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
coinStatus = coinStatus,
|
||||
)
|
||||
private fun isSendDisabled(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
coinStatus: CryptoCurrencyStatus?,
|
||||
): Boolean = cryptoCurrencyStatus.value.amount.isNullOrZero() ||
|
||||
coinStatus?.value?.amount.isNullOrZero() ||
|
||||
currenciesRepository.hasPendingTransactions(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
coinStatus = coinStatus,
|
||||
)
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
|
|
@ -28,6 +29,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.feature.swap.converters.*
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
|
|
@ -62,49 +64,60 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
currencyList: List<CryptoCurrency>,
|
||||
): PairsWithProviders {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
val initial = NetworkLeastTokenInfo(
|
||||
contractAddress = initialCurrency.contractAddress,
|
||||
network = initialCurrency.network,
|
||||
)
|
||||
val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }
|
||||
|
||||
val pairs = async {
|
||||
getPairsInternal(
|
||||
from = arrayListOf(initial),
|
||||
to = currenciesList,
|
||||
try {
|
||||
val initial = NetworkLeastTokenInfo(
|
||||
contractAddress = initialCurrency.contractAddress,
|
||||
network = initialCurrency.network,
|
||||
)
|
||||
}
|
||||
val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }
|
||||
|
||||
val reversedPairs = async {
|
||||
getPairsInternal(
|
||||
from = currenciesList,
|
||||
to = arrayListOf(initial),
|
||||
val pairsDeferred = async {
|
||||
getPairsInternal(
|
||||
from = arrayListOf(initial),
|
||||
to = currenciesList,
|
||||
)
|
||||
}
|
||||
|
||||
val reversedPairsDeferred = async {
|
||||
getPairsInternal(
|
||||
from = currenciesList,
|
||||
to = arrayListOf(initial),
|
||||
)
|
||||
}
|
||||
|
||||
val pairs = pairsDeferred.await().getOrThrow()
|
||||
val reversedPairs = reversedPairsDeferred.await().getOrThrow()
|
||||
|
||||
val allPairs = pairs + reversedPairs
|
||||
|
||||
val providers = tangemExpressApi.getProviders().getOrThrow()
|
||||
|
||||
return@withContext swapPairInfoConverter.convert(
|
||||
SwapPairsWithProviders(
|
||||
swapPair = allPairs,
|
||||
providers = providers,
|
||||
),
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
if (exception is ApiResponseError.HttpException) {
|
||||
throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: ""))
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
val allPairs = pairs.await() + reversedPairs.await()
|
||||
|
||||
val providers = tangemExpressApi.getProviders().getOrThrow()
|
||||
|
||||
return@withContext swapPairInfoConverter.convert(
|
||||
SwapPairsWithProviders(
|
||||
swapPair = allPairs,
|
||||
providers = providers,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getPairsInternal(
|
||||
from: List<NetworkLeastTokenInfo>,
|
||||
to: List<NetworkLeastTokenInfo>,
|
||||
): List<SwapPair> {
|
||||
): ApiResponse<List<SwapPair>> {
|
||||
return tangemExpressApi.getPairs(
|
||||
PairsRequestBody(
|
||||
from = from,
|
||||
to = to,
|
||||
),
|
||||
).getOrThrow()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getExchangeStatus(txId: String): Either<UnknownError, ExchangeStatusModel> {
|
||||
|
|
|
|||
|
|
@ -461,10 +461,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount,
|
||||
currencyToSend.symbol,
|
||||
),
|
||||
fromAmountValue = amount.value,
|
||||
toAmount = amountFormatter.formatSwapAmountToUI(
|
||||
swapData.toTokenAmount,
|
||||
currencyToGet.symbol,
|
||||
),
|
||||
toAmountValue = swapData.toTokenAmount.value,
|
||||
txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
)
|
||||
|
|
@ -541,10 +543,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount,
|
||||
currencyToSend.currency.symbol,
|
||||
),
|
||||
fromAmountValue = amount.value,
|
||||
toAmount = amountFormatter.formatSwapAmountToUI(
|
||||
exchangeData.dataModel.toTokenAmount,
|
||||
currencyToGet.currency.symbol,
|
||||
),
|
||||
toAmountValue = exchangeData.dataModel.toTokenAmount.value,
|
||||
txAddress = userWalletManager.getLastTransactionHash(
|
||||
currencyToSend.currency.network.backendId,
|
||||
derivationPath,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.feature.swap.domain.models
|
||||
|
||||
class ExpressException(val dataError: DataError) : Exception()
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
package com.tangem.feature.swap.domain.models.ui
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
sealed class TxState {
|
||||
|
||||
data class TxSent(
|
||||
val fromAmount: String? = null,
|
||||
val fromAmountValue: BigDecimal? = null,
|
||||
val toAmount: String? = null,
|
||||
val toAmountValue: BigDecimal? = null,
|
||||
val txAddress: String,
|
||||
val txExternalUrl: String? = null,
|
||||
val timestamp: Long,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ sealed class ProviderState {
|
|||
val subtitle: TextReference,
|
||||
val selectionType: SelectionType,
|
||||
val additionalBadge: AdditionalBadge,
|
||||
val percentLowerThenBest: Float = 0f,
|
||||
val percentLowerThenBest: PercentLowerThanBest = PercentLowerThanBest.Empty,
|
||||
override val onProviderClick: (String) -> Unit,
|
||||
) : ProviderState()
|
||||
|
||||
|
|
@ -50,18 +50,35 @@ sealed class ProviderState {
|
|||
}
|
||||
}
|
||||
|
||||
sealed class PercentLowerThanBest {
|
||||
data class Value(val value: Float) : PercentLowerThanBest()
|
||||
object Empty : PercentLowerThanBest()
|
||||
}
|
||||
|
||||
object ProviderPercentDiffComparator : Comparator<ProviderState> {
|
||||
override fun compare(o1: ProviderState, o2: ProviderState): Int {
|
||||
if (o1 is ProviderState.Content && o2 !is ProviderState.Content) {
|
||||
return 1
|
||||
}
|
||||
if (o1 !is ProviderState.Content && o2 is ProviderState.Content) {
|
||||
return -1
|
||||
}
|
||||
return if (o1 is ProviderState.Content && o2 is ProviderState.Content) {
|
||||
o1.percentLowerThenBest.compareTo(o2.percentLowerThenBest)
|
||||
if (o1 !is ProviderState.Content && o2 is ProviderState.Content) {
|
||||
return 1
|
||||
}
|
||||
if (o1 is ProviderState.Content && o2 is ProviderState.Content) {
|
||||
val o1Percent = o1.percentLowerThenBest
|
||||
val o2Percent = o2.percentLowerThenBest
|
||||
if (o1Percent is PercentLowerThanBest.Value && o2Percent !is PercentLowerThanBest.Value) {
|
||||
return -1
|
||||
}
|
||||
if (o1Percent !is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) {
|
||||
return 1
|
||||
}
|
||||
return if (o1Percent is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) {
|
||||
o1Percent.value.compareTo(o2Percent.value)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
|
||||
import com.tangem.feature.swap.models.states.PercentLowerThanBest
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -89,7 +90,7 @@ private fun ChooseProviderBottomSheet_Preview() {
|
|||
iconUrl = "",
|
||||
subtitle = stringReference("1 000 000"),
|
||||
additionalBadge = ProviderState.AdditionalBadge.BestTrade,
|
||||
percentLowerThenBest = -1.0f,
|
||||
percentLowerThenBest = PercentLowerThanBest.Value(-1.0f),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
onProviderClick = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.core.ui.components.SpacerH24
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.states.PercentLowerThanBest
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
|
||||
/**
|
||||
|
|
@ -151,8 +152,10 @@ private fun ProviderContentState(
|
|||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (state.percentLowerThenBest > 0f) {
|
||||
AnimatedContent(targetState = state.percentLowerThenBest, label = "") {
|
||||
if (state.percentLowerThenBest is PercentLowerThanBest.Value &&
|
||||
state.percentLowerThenBest.value > 0
|
||||
) {
|
||||
AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") {
|
||||
Text(
|
||||
text = "-$it%",
|
||||
style = TangemTheme.typography.body2,
|
||||
|
|
@ -404,7 +407,7 @@ private fun ProviderItem_Content_Preview() {
|
|||
iconUrl = "",
|
||||
subtitle = stringReference("1 000 000"),
|
||||
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
|
||||
percentLowerThenBest = -1.0f,
|
||||
percentLowerThenBest = PercentLowerThanBest.Value(-1.0f),
|
||||
selectionType = ProviderState.SelectionType.SELECT,
|
||||
onProviderClick = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -579,14 +579,14 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
fun createInitialErrorState(uiState: SwapStateHolder, onRefreshClick: () -> Unit): SwapStateHolder {
|
||||
fun createInitialErrorState(uiState: SwapStateHolder, code: Int, onRefreshClick: () -> Unit): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
warnings = listOf(
|
||||
SwapWarning.GeneralWarning(
|
||||
notificationConfig = NotificationConfig(
|
||||
title = TextReference.Res(R.string.warning_express_refresh_required_title),
|
||||
subtitle = TextReference.EMPTY,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = TextReference.Res(R.string.warning_button_refresh),
|
||||
onClick = onRefreshClick,
|
||||
|
|
@ -639,7 +639,7 @@ internal class StateBuilder(
|
|||
@Suppress("LongParameterList")
|
||||
fun createSuccessState(
|
||||
uiState: SwapStateHolder,
|
||||
timeStamp: Long,
|
||||
txState: TxState.TxSent,
|
||||
txUrl: String,
|
||||
dataState: SwapProcessDataState,
|
||||
onExploreClick: () -> Unit,
|
||||
|
|
@ -648,18 +648,16 @@ internal class StateBuilder(
|
|||
val fee = requireNotNull(dataState.selectedFee)
|
||||
val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency)
|
||||
val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency)
|
||||
val fromAmount = toBigDecimalOrNull(requireNotNull(dataState.amount))
|
||||
val toAmount = requireNotNull(dataState.swapDataModel?.toTokenAmount?.value)
|
||||
val fromAmount = txState.fromAmountValue ?: BigDecimal.ZERO
|
||||
val toAmount = txState.toAmountValue ?: BigDecimal.ZERO
|
||||
val providerState = uiState.providerState as ProviderState.Content
|
||||
|
||||
val fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount(fromAmount, fromCryptoCurrency.currency)
|
||||
val toCryptoAmount = BigDecimalFormatter.formatCryptoAmount(toAmount, toCryptoCurrency.currency)
|
||||
val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount))
|
||||
val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount))
|
||||
|
||||
return uiState.copy(
|
||||
successState = SwapSuccessStateHolder(
|
||||
timestamp = timeStamp,
|
||||
timestamp = txState.timestamp,
|
||||
txUrl = txUrl,
|
||||
providerName = stringReference(providerState.name),
|
||||
providerType = stringReference(providerState.type),
|
||||
|
|
@ -667,8 +665,8 @@ internal class StateBuilder(
|
|||
providerIcon = providerState.iconUrl,
|
||||
rate = providerState.subtitle,
|
||||
fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"),
|
||||
fromTokenAmount = stringReference(fromCryptoAmount),
|
||||
toTokenAmount = stringReference(toCryptoAmount),
|
||||
fromTokenAmount = stringReference(txState.fromAmount.orEmpty()),
|
||||
toTokenAmount = stringReference(txState.toAmount.orEmpty()),
|
||||
fromTokenFiatAmount = stringReference(fromFiatAmount),
|
||||
toTokenFiatAmount = stringReference(toFiatAmount),
|
||||
fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency),
|
||||
|
|
@ -789,7 +787,7 @@ internal class StateBuilder(
|
|||
fun showSelectProviderBottomSheet(
|
||||
uiState: SwapStateHolder,
|
||||
selectedProviderId: String,
|
||||
pricesLowerBest: Map<SwapProvider, Float>,
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
providersStates: Map<SwapProvider, SwapState>,
|
||||
unavailableProviders: List<SwapProvider>,
|
||||
onDismiss: () -> Unit,
|
||||
|
|
@ -820,6 +818,7 @@ internal class StateBuilder(
|
|||
|
||||
fun updateProvidersBottomSheetContent(
|
||||
uiState: SwapStateHolder,
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
tokenSwapInfoForProviders: Map<String, TokenSwapInfo>,
|
||||
): SwapStateHolder {
|
||||
val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig
|
||||
|
|
@ -835,6 +834,9 @@ internal class StateBuilder(
|
|||
.getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency)
|
||||
it.copy(
|
||||
subtitle = stringReference(rateString),
|
||||
percentLowerThenBest = pricesLowerBest[it.id]?.let { percent ->
|
||||
PercentLowerThanBest.Value(percent)
|
||||
} ?: PercentLowerThanBest.Empty,
|
||||
)
|
||||
} else {
|
||||
it
|
||||
|
|
@ -928,7 +930,7 @@ internal class StateBuilder(
|
|||
}
|
||||
|
||||
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderBottomSheetState(
|
||||
pricesLowerBest: Map<SwapProvider, Float>,
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
onProviderSelect: (String) -> Unit,
|
||||
): ProviderState? {
|
||||
val provider = this.key
|
||||
|
|
@ -1040,7 +1042,7 @@ internal class StateBuilder(
|
|||
subtitle = stringReference(rateString),
|
||||
additionalBadge = badge,
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = ZERO_PERCENT,
|
||||
percentLowerThenBest = PercentLowerThanBest.Empty,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -1049,7 +1051,7 @@ internal class StateBuilder(
|
|||
isBestRate: Boolean,
|
||||
state: SwapState.QuotesLoadedState,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
pricesLowerBest: Map<SwapProvider, Float>,
|
||||
pricesLowerBest: Map<String, Float>,
|
||||
onProviderClick: (String) -> Unit,
|
||||
): ProviderState {
|
||||
val toTokenInfo = state.toTokenInfo
|
||||
|
|
@ -1069,7 +1071,9 @@ internal class StateBuilder(
|
|||
subtitle = stringReference(rateString),
|
||||
additionalBadge = additionalBadge,
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = pricesLowerBest[this] ?: ZERO_PERCENT,
|
||||
percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent ->
|
||||
PercentLowerThanBest.Value(percent)
|
||||
} ?: PercentLowerThanBest.Value(0f),
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -1103,7 +1107,7 @@ internal class StateBuilder(
|
|||
selectionType = selectionType,
|
||||
subtitle = alertText,
|
||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||
percentLowerThenBest = ZERO_PERCENT,
|
||||
percentLowerThenBest = PercentLowerThanBest.Empty,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -1148,6 +1152,5 @@ internal class StateBuilder(
|
|||
private const val HUNDRED_PERCENTS = 100
|
||||
private const val UNKNOWN_AMOUNT_SIGN = "—"
|
||||
private const val MAX_DECIMALS_TO_SHOW = 8
|
||||
private const val ZERO_PERCENT = 0f
|
||||
}
|
||||
}
|
||||
|
|
@ -2,21 +2,33 @@ package com.tangem.feature.swap.ui
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
|
||||
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
|
||||
import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
|
||||
@Composable
|
||||
internal fun SwapScreen(stateHolder: SwapStateHolder) {
|
||||
BackHandler(onBack = stateHolder.onBackClicked)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
text = stringResource(R.string.common_swap),
|
||||
onBackClick = stateHolder.onBackClicked,
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
)
|
||||
},
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
) { scaffoldPaddings ->
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.tangem.common.Strings.STARS
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.getActiveIconResByCoinId
|
||||
|
|
@ -40,12 +39,6 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
|
|||
.background(color = TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
Column {
|
||||
AppBarWithBackButton(
|
||||
text = stringResource(R.string.common_swap),
|
||||
onBackClick = state.onBackClicked,
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier =
|
|||
LazyColumn(
|
||||
modifier = modifier
|
||||
.background(color = screenBackgroundColor)
|
||||
.fillMaxSize(),
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
item { SpacerH8() }
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.mapNotNull
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.utils.InputNumberFormatter
|
||||
|
|
@ -19,6 +18,7 @@ import com.tangem.feature.swap.analytics.SwapEvents
|
|||
import com.tangem.feature.swap.domain.BlockchainInteractor
|
||||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
|
|
@ -175,13 +175,19 @@ internal class SwapViewModel @Inject constructor(
|
|||
selectedCurrency = null,
|
||||
)
|
||||
|
||||
uiState = stateBuilder.createInitialErrorState(uiState) {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
initialCurrency = initialCryptoCurrency,
|
||||
networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId),
|
||||
)
|
||||
initTokens()
|
||||
}
|
||||
uiState =
|
||||
stateBuilder.createInitialErrorState(
|
||||
uiState,
|
||||
(it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code,
|
||||
) {
|
||||
uiState = stateBuilder.createInitialLoadingState(
|
||||
initialCurrency = initialCryptoCurrency,
|
||||
networkInfo = blockchainInteractor.getBlockchainInfo(
|
||||
initialCryptoCurrency.network.backendId,
|
||||
),
|
||||
)
|
||||
initTokens()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -289,11 +295,13 @@ internal class SwapViewModel @Inject constructor(
|
|||
if (providersState.isNotEmpty()) {
|
||||
val (provider, state) = updateLoadedQuotes(providersState)
|
||||
setupLoadedState(provider, state, fromToken)
|
||||
val successStates = providersState
|
||||
.getLastLoadedSuccessStates()
|
||||
val pricesLowerBest = getPricesLowerBest(successStates)
|
||||
uiState = stateBuilder.updateProvidersBottomSheetContent(
|
||||
uiState = uiState,
|
||||
tokenSwapInfoForProviders = providersState
|
||||
.getLastLoadedSuccessStates()
|
||||
.entries
|
||||
pricesLowerBest = pricesLowerBest,
|
||||
tokenSwapInfoForProviders = successStates.entries
|
||||
.associate { it.key.providerId to it.value.toTokenInfo },
|
||||
)
|
||||
} else {
|
||||
|
|
@ -384,7 +392,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
val stateSuccess = state.getLastLoadedSuccessStates()
|
||||
return if (stateSuccess.isNotEmpty()) {
|
||||
val currentSelected = dataState.selectedProvider
|
||||
if (currentSelected != null && state.keys.contains(currentSelected)) {
|
||||
if (currentSelected != null && stateSuccess.keys.contains(currentSelected)) {
|
||||
currentSelected
|
||||
} else {
|
||||
findBestQuoteProvider(stateSuccess) ?: stateSuccess.keys.first()
|
||||
|
|
@ -457,7 +465,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
)
|
||||
uiState = stateBuilder.createSuccessState(
|
||||
uiState = uiState,
|
||||
timeStamp = it.timestamp,
|
||||
txState = it,
|
||||
dataState = dataState,
|
||||
txUrl = url,
|
||||
onExploreClick = {
|
||||
|
|
@ -869,21 +877,21 @@ internal class SwapViewModel @Inject constructor(
|
|||
}?.key
|
||||
}
|
||||
|
||||
private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map<SwapProvider, Float> {
|
||||
private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map<String, Float> {
|
||||
val bestRateEntry = state.maxByOrNull { it.value.toTokenInfo.tokenAmount.value } ?: return emptyMap()
|
||||
val bestRate = bestRateEntry.value.toTokenInfo.tokenAmount.value
|
||||
val hundredPercent = BigDecimal("100")
|
||||
return state.mapNotNull {
|
||||
return state.entries.mapNotNull {
|
||||
if (it.key != bestRateEntry.key) {
|
||||
val amount = it.value.toTokenInfo.tokenAmount.value
|
||||
val percentDiff = BigDecimal.ONE.minus(
|
||||
amount.divide(bestRate, RoundingMode.HALF_UP),
|
||||
).multiply(hundredPercent)
|
||||
percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue
|
||||
it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ spr-client = "3.6.2"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "release-app_5.4-412"
|
||||
tangemBlockchainSdk = "release-app_5.4-416"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "release-app_5.4-315"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue