Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-05 09:12:03 +00:00
commit 3f386274ec
286 changed files with 4852 additions and 2134 deletions

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.feature.swap.presentation" />

View file

@ -15,7 +15,6 @@ import com.tangem.feature.swap.models.states.ProviderState
data class SwapStateHolder(
val sendCardData: SwapCardState,
val receiveCardData: SwapCardState,
val networkCurrency: String,
val blockchainId: String, // not the same as networkId, its local id in app
val warnings: List<SwapWarning> = emptyList(),
val alert: SwapWarning.GenericWarning? = null,

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.models
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.ui.TxFee
data class UiActions(
@ -20,7 +21,7 @@ data class UiActions(
val onSelectFeeType: (TxFee) -> Unit,
val onProviderClick: (String) -> Unit,
val onProviderSelect: (String) -> Unit,
val onBuyClick: () -> Unit,
val onBuyClick: (CryptoCurrency) -> Unit,
val onPolicyClick: (String) -> Unit,
val onTosClick: (String) -> Unit,
val onReceiveCardWarningClick: () -> Unit,

View file

@ -27,8 +27,8 @@ sealed class ProviderState {
val subtitle: TextReference,
val selectionType: SelectionType,
val additionalBadge: AdditionalBadge,
val percentLowerThenBest: PercentDifference = PercentDifference.Empty,
val namePrefix: PrefixType,
val percentLowerThenBest: PercentLowerThanBest = PercentLowerThanBest.Empty,
override val onProviderClick: (String) -> Unit,
) : ProviderState()
@ -61,9 +61,9 @@ sealed class ProviderState {
}
@Immutable
sealed class PercentLowerThanBest {
data class Value(val value: Float) : PercentLowerThanBest()
object Empty : PercentLowerThanBest()
sealed class PercentDifference {
data class Value(val value: Float) : PercentDifference()
object Empty : PercentDifference()
}
object ProviderPercentDiffComparator : Comparator<ProviderState> {
@ -77,14 +77,14 @@ object ProviderPercentDiffComparator : Comparator<ProviderState> {
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) {
if (o1Percent is PercentDifference.Value && o2Percent !is PercentDifference.Value) {
return -1
}
if (o1Percent !is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) {
if (o1Percent !is PercentDifference.Value && o2Percent is PercentDifference.Value) {
return 1
}
return if (o1Percent is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) {
o1Percent.value.compareTo(o2Percent.value)
return if (o1Percent is PercentDifference.Value && o2Percent is PercentDifference.Value) {
o2Percent.value.compareTo(o1Percent.value)
} else {
0
}

View file

@ -10,7 +10,6 @@ import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -31,12 +30,13 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun AutoSizeTextField(
textFieldValue: TextFieldValue,
focusRequester: FocusRequester,
onAmountChange: (String) -> Unit,
onFocusChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val textFieldFocusRequester = remember { FocusRequester() }
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
var shrunkFontSize = TangemTheme.typography.h2.fontSize
val calculateIntrinsics = @Composable {
@ -71,7 +71,7 @@ internal fun AutoSizeTextField(
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.focusRequester(textFieldFocusRequester)
.focusRequester(focusRequester)
.onFocusChanged { onFocusChange(it.hasFocus) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,

View file

@ -19,7 +19,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.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toImmutableList
@ -114,7 +114,7 @@ private fun ChooseProviderBottomSheet_Preview() {
iconUrl = "",
subtitle = stringReference("1 000 000"),
additionalBadge = ProviderState.AdditionalBadge.BestTrade,
percentLowerThenBest = PercentLowerThanBest.Value(-1.0f),
percentLowerThenBest = PercentDifference.Value(-1.0f),
selectionType = ProviderState.SelectionType.SELECT,
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = {},

View file

@ -26,7 +26,7 @@ import com.tangem.core.ui.components.RectangleShimmer
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.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
/**
@ -94,11 +94,8 @@ private fun ProviderContentState(
.padding(start = TangemTheme.dimens.spacing12)
.size(size = TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(state.iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl)
.crossfade(enable = true).allowHardware(false).build(),
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
error = {
ErrorProviderIcon(
@ -138,10 +135,12 @@ private fun ProviderContentState(
)
}
when (state.additionalBadge) {
ProviderState.AdditionalBadge.BestTrade ->
BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4))
ProviderState.AdditionalBadge.PermissionRequired ->
PermissionBadgeItem(Modifier.padding(start = TangemTheme.dimens.spacing4))
ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(
Modifier.padding(start = TangemTheme.dimens.spacing4),
)
ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(
Modifier.padding(start = TangemTheme.dimens.spacing4),
)
ProviderState.AdditionalBadge.Empty -> {
// no-op
}
@ -162,14 +161,19 @@ private fun ProviderContentState(
maxLines = 1,
)
}
if (state.percentLowerThenBest is PercentLowerThanBest.Value &&
state.percentLowerThenBest.value > 0
if (state.percentLowerThenBest is PercentDifference.Value &&
state.percentLowerThenBest.value != 0f
) {
val textColor = if (state.percentLowerThenBest.value > 0) {
TangemTheme.colors.icon.accent
} else {
TangemTheme.colors.text.warning
}
AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") {
Text(
text = "-$it%",
text = if (it > 0) "+$it%" else "$it%",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.warning,
color = textColor,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
@ -198,11 +202,8 @@ private fun ProviderUnavailableState(
.padding(start = TangemTheme.dimens.spacing12)
.size(size = TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(state.iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl)
.crossfade(enable = true).allowHardware(false).build(),
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
error = {
ErrorProviderIcon(
@ -298,8 +299,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) {
@Composable
private fun BoxScope.ProviderChevron(selectionType: ProviderState.SelectionType, isSelected: Boolean) {
when (selectionType) {
ProviderState.SelectionType.NONE -> {
/* no-op */
ProviderState.SelectionType.NONE -> { /* no-op */
}
ProviderState.SelectionType.CLICK -> {
Icon(
@ -345,11 +345,10 @@ private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable Bo
@Composable
private fun ErrorProviderIcon(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCorners8,
),
modifier = modifier.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCorners8,
),
contentAlignment = Alignment.Center,
) {
Icon(
@ -369,7 +368,7 @@ private fun BestTradeItem(modifier: Modifier = Modifier) {
),
) {
Text(
text = "Best rate",
text = stringResource(R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6),
@ -432,7 +431,7 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider
iconUrl = "",
subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"),
additionalBadge = ProviderState.AdditionalBadge.Empty,
percentLowerThenBest = PercentLowerThanBest.Empty,
percentLowerThenBest = PercentDifference.Value(value = 12.0f),
selectionType = ProviderState.SelectionType.SELECT,
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
onProviderClick = {},
@ -440,7 +439,7 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider
val contentState2 = contentState.copy(
subtitle = stringReference(value = "1 132,46 MATIC"),
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
percentLowerThenBest = PercentLowerThanBest.Value(value = 5f),
percentLowerThenBest = PercentDifference.Value(value = 5f),
)
val unavailableState = ProviderState.Unavailable(
id = "1",

View file

@ -79,7 +79,6 @@ internal class StateBuilder(
isBalanceHidden = true,
),
fee = FeeItemState.Empty,
networkCurrency = networkInfo.blockchainCurrency,
swapButton = SwapButton(enabled = false, onClick = {}),
onRefresh = {},
onBackClicked = actions.onBackClicked,
@ -250,7 +249,6 @@ internal class StateBuilder(
balance = toCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
isBalanceHidden = isBalanceHiddenProvider(),
),
networkCurrency = quoteModel.networkCurrency,
warnings = warnings,
permissionState = convertPermissionState(
lastPermissionState = uiStateHolder.permissionState,
@ -317,7 +315,7 @@ internal class StateBuilder(
val warnings = mutableListOf<SwapWarning>()
addDomainWarnings(quoteModel, warnings)
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
quoteModel.preparedSwapConfigState.isFeeEnough &&
quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
) {
warnings.add(
@ -412,7 +410,8 @@ internal class StateBuilder(
fromToken: CryptoCurrency,
warnings: MutableList<SwapWarning>,
) {
if (!quoteModel.preparedSwapConfigState.isFeeEnough &&
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState
if (feeEnoughState is SwapFeeState.NotEnough &&
quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.permissionState !is PermissionDataState.PermissionLoading
) {
@ -420,7 +419,9 @@ internal class StateBuilder(
SwapWarning.UnableToCoverFeeWarning(
createUnableToCoverFeeNotificationConfig(
fromToken = fromToken,
onBuyClick = actions.onBuyClick,
feeCurrency = feeEnoughState.feeCurrency,
currencyName = feeEnoughState.currencyName ?: fromToken.network.name,
currencySymbol = feeEnoughState.currencySymbol ?: fromToken.network.currencySymbol,
),
),
)
@ -438,7 +439,7 @@ internal class StateBuilder(
IncludeFeeInAmount.Excluded ->
preparedSwapConfigState.isAllowedToSpend &&
preparedSwapConfigState.isBalanceEnough &&
preparedSwapConfigState.isFeeEnough
preparedSwapConfigState.feeState is SwapFeeState.Enough
is IncludeFeeInAmount.Included -> true
}
}
@ -693,6 +694,32 @@ internal class StateBuilder(
)
}
fun updateSendCurrencyBalance(
uiState: SwapStateHolder,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): SwapStateHolder {
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
return uiState.copy(
sendCardData = uiState.sendCardData.copy(
balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
),
)
}
fun updateReceiveCurrencyBalance(
uiState: SwapStateHolder,
cryptoCurrencyStatus: CryptoCurrencyStatus,
): SwapStateHolder {
if (uiState.receiveCardData !is SwapCardState.SwapCardData) return uiState
return uiState.copy(
receiveCardData = uiState.receiveCardData.copy(
balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
),
)
}
fun updateBalanceHiddenState(uiState: SwapStateHolder, isBalanceHidden: Boolean): SwapStateHolder {
if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState
if (uiState.receiveCardData !is SwapCardState.SwapCardData) return uiState
@ -998,6 +1025,7 @@ internal class StateBuilder(
fun showSelectProviderBottomSheet(
uiState: SwapStateHolder,
selectedProviderId: String,
bestRatedProviderId: String,
pricesLowerBest: Map<String, Float>,
providersStates: Map<SwapProvider, SwapState>,
unavailableProviders: List<SwapProvider>,
@ -1005,7 +1033,7 @@ internal class StateBuilder(
): SwapStateHolder {
val availableProvidersStates = providersStates.entries
.mapNotNull {
it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect)
it.convertToProviderBottomSheetState(pricesLowerBest, bestRatedProviderId, actions.onProviderSelect)
}
.sortedWith(ProviderPercentDiffComparator)
val unavailableProviderStates = unavailableProviders.map {
@ -1046,8 +1074,8 @@ internal class StateBuilder(
it.copy(
subtitle = stringReference(rateString),
percentLowerThenBest = pricesLowerBest[it.id]?.let { percent ->
PercentLowerThanBest.Value(percent)
} ?: PercentLowerThanBest.Empty,
PercentDifference.Value(percent)
} ?: PercentDifference.Value(0f),
)
} else {
it
@ -1093,7 +1121,7 @@ internal class StateBuilder(
},
readMoreUrl = buildReadMoreUrl(),
feeItems = txFeeState.toFeeItemState(),
readMore = resourceReference(R.string.common_fee_selector_link_description),
readMore = resourceReference(R.string.common_read_more),
onReadMoreClick = actions.onFeeReadMoreClick,
)
return uiState.copy(
@ -1153,18 +1181,21 @@ internal class StateBuilder(
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderBottomSheetState(
pricesLowerBest: Map<String, Float>,
bestRatedProviderId: String,
onProviderSelect: (String) -> Unit,
): ProviderState? {
val provider = this.key
return when (val state = this.value) {
is SwapState.EmptyAmountState -> null
is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState(
isBestRate = false, // not show best rate in bottom sheet
state = state,
onProviderClick = onProviderSelect,
pricesLowerBest = pricesLowerBest,
selectionType = ProviderState.SelectionType.SELECT,
)
is SwapState.QuotesLoadedState -> {
provider.convertToContentSelectableProviderState(
isBestRate = bestRatedProviderId == provider.providerId,
state = state,
onProviderClick = onProviderSelect,
pricesLowerBest = pricesLowerBest,
selectionType = ProviderState.SelectionType.SELECT,
)
}
is SwapState.SwapError -> getProviderStateForError(
swapProvider = provider,
fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency,
@ -1189,8 +1220,16 @@ internal class StateBuilder(
private fun createUnableToCoverFeeNotificationConfig(
fromToken: CryptoCurrency,
onBuyClick: () -> Unit,
feeCurrency: CryptoCurrency?,
currencyName: String,
currencySymbol: String,
): NotificationConfig {
val buttonState = feeCurrency?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)),
onClick = { actions.onBuyClick(it) },
)
}
return NotificationConfig(
title = resourceReference(
R.string.warning_express_not_enough_fee_for_token_tx_title,
@ -1198,20 +1237,17 @@ internal class StateBuilder(
),
subtitle = resourceReference(
R.string.warning_express_not_enough_fee_for_token_tx_description,
wrappedList(fromToken.network.name, fromToken.network.currencySymbol),
wrappedList(currencyName, currencySymbol),
),
iconResId = fromToken.networkIconResId,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_buy_currency, wrappedList(fromToken.network.currencySymbol)),
onClick = onBuyClick,
),
buttonsState = buttonState,
)
}
private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig {
return NotificationConfig(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(R.string.send_network_fee_warning_content),
subtitle = resourceReference(R.string.swapping_network_fee_warning_content),
iconResId = R.drawable.img_attention_20,
)
}
@ -1256,7 +1292,7 @@ internal class StateBuilder(
subtitle = stringReference(rateString),
additionalBadge = badge,
selectionType = selectionType,
percentLowerThenBest = PercentLowerThanBest.Empty,
percentLowerThenBest = PercentDifference.Empty,
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
onProviderClick = onProviderClick,
)
@ -1287,8 +1323,8 @@ internal class StateBuilder(
additionalBadge = additionalBadge,
selectionType = selectionType,
percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent ->
PercentLowerThanBest.Value(percent)
} ?: PercentLowerThanBest.Value(0f),
PercentDifference.Value(percent)
} ?: PercentDifference.Value(0f),
namePrefix = ProviderState.PrefixType.NONE,
onProviderClick = onProviderClick,
)
@ -1323,7 +1359,7 @@ internal class StateBuilder(
selectionType = selectionType,
subtitle = alertText,
additionalBadge = ProviderState.AdditionalBadge.Empty,
percentLowerThenBest = PercentLowerThanBest.Empty,
percentLowerThenBest = PercentDifference.Empty,
namePrefix = if (selectionType != ProviderState.SelectionType.SELECT) {
ProviderState.PrefixType.PROVIDED_BY
} else {

View file

@ -490,7 +490,6 @@ private val state = SwapStateHolder(
),
),
),
networkCurrency = "MATIC",
swapButton = SwapButton(enabled = true, onClick = {}),
onRefresh = {},
onBackClicked = {},

View file

@ -19,6 +19,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
@ -262,12 +263,19 @@ private fun Content(
}
}
is TransactionCardType.Inputtable -> {
val focusRequester = remember { FocusRequester() }
AutoSizeTextField(
modifier = sumTextModifier,
focusRequester = focusRequester,
textFieldValue = textFieldValue ?: TextFieldValue(),
onAmountChange = { type.onAmountChanged(it) },
onFocusChange = type.onFocusChanged,
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
@ -14,11 +15,13 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
@ -47,7 +50,6 @@ import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
import javax.inject.Inject
import kotlin.math.absoluteValue
import kotlin.properties.Delegates
typealias SuccessLoadedSwapData = Map<SwapProvider, SwapState.QuotesLoadedState>
@ -63,6 +65,7 @@ internal class SwapViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
@ -105,6 +108,9 @@ internal class SwapViewModel @Inject constructor(
(it.error is DataError.ExchangeTooSmallAmountError || it.error is DataError.ExchangeTooBigAmountError)
}
private val fromTokenBalanceJobHolder = JobHolder()
private val toTokenBalanceJobHolder = JobHolder()
val currentScreen: SwapNavScreen
get() = swapRouter.currentScreen
@ -174,6 +180,23 @@ internal class SwapViewModel @Inject constructor(
state,
),
)
val userWalletId = swapInteractor.getSelectedWallet()?.walletId ?: return@launch
(dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = it,
isFromCurrency = true,
)
}
(dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = it,
isFromCurrency = false,
)
}
}.onFailure {
Timber.tag(loggingTag).e(it)
@ -302,7 +325,7 @@ internal class SwapViewModel @Inject constructor(
val (provider, state) = updateLoadedQuotes(providersState)
setupLoadedState(provider, state, fromToken)
val successStates = providersState.getLastLoadedSuccessStates()
val pricesLowerBest = getPricesLowerBest(successStates)
val pricesLowerBest = getPricesLowerBest(provider.providerId, successStates)
uiState = stateBuilder.updateProvidersBottomSheetContent(
uiState = uiState,
pricesLowerBest = pricesLowerBest,
@ -623,7 +646,6 @@ internal class SwapViewModel @Inject constructor(
unavailable = unavailable,
afterSearch = true,
),
)
} else {
tokenDataState.copy(
@ -653,15 +675,35 @@ internal class SwapViewModel @Inject constructor(
analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it))
}
val userWalletId = swapInteractor.getSelectedWallet()?.walletId
if (foundToken != null) {
val fromToken: CryptoCurrencyStatus
val toToken: CryptoCurrencyStatus
if (isOrderReversed) {
fromToken = foundToken.currencyStatus
toToken = initialCryptoCurrencyStatus
val newToken = fromToken.currency as? CryptoCurrency.Coin
if (userWalletId != null && newToken != null) {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = newToken,
isFromCurrency = true,
)
}
} else {
fromToken = initialCryptoCurrencyStatus
toToken = foundToken.currencyStatus
val newToken = toToken.currency as? CryptoCurrency.Coin
if (userWalletId != null && newToken != null) {
subscribeToCoinBalanceUpdates(
userWalletId = userWalletId,
coin = newToken,
isFromCurrency = false,
)
}
}
dataState = dataState.copy(
fromCryptoCurrency = fromToken,
@ -679,6 +721,36 @@ internal class SwapViewModel @Inject constructor(
}
}
private fun subscribeToCoinBalanceUpdates(
userWalletId: UserWalletId,
coin: CryptoCurrency.Coin,
isFromCurrency: Boolean,
) {
Timber.d("Subscribe to ${coin.id} balance updates")
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = coin.id,
isSingleWalletWithTokens = false,
)
.mapNotNull { (it as? Either.Right)?.value }
.distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes
.onEach {
Timber.d("${coin.id} balance is ${it.value.amount}")
uiState = if (isFromCurrency) {
dataState = dataState.copy(fromCryptoCurrency = it)
stateBuilder.updateSendCurrencyBalance(uiState, it)
} else {
dataState = dataState.copy(toCryptoCurrency = it)
stateBuilder.updateReceiveCurrencyBalance(uiState, it)
}
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
.saveIn(if (isFromCurrency) fromTokenBalanceJobHolder else toTokenBalanceJobHolder)
}
private fun onChangeCardsClicked() {
val newFromToken = dataState.toCryptoCurrency
val newToToken = dataState.fromCryptoCurrency
@ -847,13 +919,14 @@ internal class SwapViewModel @Inject constructor(
onProviderClick = { providerId ->
analyticsEventHandler.send(SwapEvents.ProviderClicked)
val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates()
val pricesLowerBest = getPricesLowerBest(states)
val pricesLowerBest = getPricesLowerBest(providerId, states)
val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates)
uiState = stateBuilder.showSelectProviderBottomSheet(
uiState = uiState,
selectedProviderId = providerId,
pricesLowerBest = pricesLowerBest,
unavailableProviders = unavailableProviders,
bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId,
providersStates = dataState.lastLoadedSwapStates,
) { uiState = stateBuilder.dismissBottomSheet(uiState) }
},
@ -871,12 +944,11 @@ internal class SwapViewModel @Inject constructor(
)
}
},
onBuyClick = {
swapInteractor.getSelectedWallet()?.let {
val fromToken = dataState.fromCryptoCurrency ?: return@let
onBuyClick = { currency ->
swapInteractor.getSelectedWallet()?.let { userWallet ->
swapRouter.openTokenDetails(
it.walletId,
swapInteractor.getNativeToken(fromToken.currency.network.backendId),
userWalletId = userWallet.walletId,
currency = currency,
)
}
},
@ -962,17 +1034,18 @@ internal class SwapViewModel @Inject constructor(
}?.key
}
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
private fun getPricesLowerBest(selectedProviderId: String, state: SuccessLoadedSwapData): Map<String, Float> {
val selectedProviderEntry = state.filter { it.key.providerId == selectedProviderId }.entries.firstOrNull()
?: return emptyMap()
val selectedProviderRate = selectedProviderEntry.value.toTokenInfo.tokenAmount.value
val hundredPercent = BigDecimal("100")
return state.entries.mapNotNull {
if (it.key != bestRateEntry.key) {
if (it.key != selectedProviderEntry.key) {
val amount = it.value.toTokenInfo.tokenAmount.value
val percentDiff = BigDecimal.ONE.minus(
amount.divide(bestRate, RoundingMode.HALF_UP),
selectedProviderRate.divide(amount, RoundingMode.HALF_UP),
).multiply(hundredPercent)
it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue
it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat()
} else {
null
}