From b38cfdcf9ef38a203528aa25df87bfd3663bbd68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 16:21:41 +0300 Subject: [PATCH 1/5] Updated on 2026-08-14 --- .../NotificationWithBackground.kt | 14 ++++++------ .../main/res/drawable/ic_swap_promo_34.xml | 20 ------------------ .../src/main/res/drawable/img_swap_promo.webp | Bin 0 -> 2284 bytes .../components/TokenDetailsNotification.kt | 2 +- .../state/components/WalletNotification.kt | 2 +- 5 files changed, 10 insertions(+), 28 deletions(-) delete mode 100644 core/ui/src/main/res/drawable/ic_swap_promo_34.xml create mode 100644 core/ui/src/main/res/drawable/img_swap_promo.webp diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 072a7389f6..b377102b35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -66,10 +66,12 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Image( painter = painterResource(config.iconResId), contentDescription = null, - modifier = Modifier.constrainAs(iconRef) { - start.linkTo(parent.start, spacing12) - linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) - }, + modifier = Modifier + .size(TangemTheme.dimens.size34) + .constrainAs(iconRef) { + start.linkTo(parent.start, spacing12) + linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) + }, ) Text( text = config.title.resolveReference(), @@ -147,7 +149,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid NotificationConfig( title = resourceReference(id = R.string.main_swap_promotion_title), subtitle = resourceReference(id = R.string.main_swap_promotion_message), - iconResId = R.drawable.ic_swap_promo_34, + iconResId = R.drawable.img_swap_promo, backgroundResId = R.drawable.img_swap_promo_banner_background, ), NotificationConfig( @@ -158,7 +160,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid "currencies between any chains you wish. Commission free period " + "till Dec 31.", ), - iconResId = R.drawable.ic_swap_promo_34, + iconResId = R.drawable.img_swap_promo, backgroundResId = R.drawable.img_swap_promo_banner_background, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(id = R.string.token_swap_promotion_button), diff --git a/core/ui/src/main/res/drawable/ic_swap_promo_34.xml b/core/ui/src/main/res/drawable/ic_swap_promo_34.xml deleted file mode 100644 index 3c279910c8..0000000000 --- a/core/ui/src/main/res/drawable/ic_swap_promo_34.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/core/ui/src/main/res/drawable/img_swap_promo.webp b/core/ui/src/main/res/drawable/img_swap_promo.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f5bd02d24fb43d2b4f97dbd1e6fcde14cca4170 GIT binary patch literal 2284 zcmVHC>gEYuk<6JV4GoG@@mOZqj{LX&@p9{0}*@ZPiMD z+fGj9x?)Kg8z3(MpXuuW7LG~Z4f(H5@w^KNiGl`@&jYbB|wX!Hx zl2V%`&4?vREbEeNlqA!HvMS9cSY}$3^^f?W)oYujnnsjr)skX~8EdIcrjabxh|Ne` zs+C01Xc`2rMFR#Zii8-01QH>D2r3XL0vJpPU@Q_mE83@GLNNjV6NbZ;i!`F5DaGUA zWbtX}lL%lmiyDy#WqFdYG%IUxOB9GlM59@;PmVq(k1Q4s- z%Sc+z!#-S()`5}C));-*W3*a#a?Dt3yE&!nPtd5A5ssUTbuHai z#z2-9_8nu!8gqnGJEw5{2_0({cghH>b+G?tN!zwt+nUjVV`-7J0WmP>NJr9^X}ibu z*s{m=NQ<%0mk)1`cjuIz9``;!PdBG970ybt!dc-ID01ZgSz#(ng(-k0Xo4na8aKfs zXimi{cm&OOXimjtdNSh-oO1}WZ90GK=ME7u0nl8EwyhnKq-=N3JbT%;?aIGw zEB~^UjEFCd@>;?B$Da=n(SO*I4g9{|DtnG*u_2@-AFG9)P~#%|j-2W@xuE3|Ff zwv8Vh+s=-Rd@r&;Afo>cBuQ$U-g1G3b3lY2c;xBFM>dm;#Yk5yp2%cBJ$a}f*~Aw) zE0KH8w+NX91@gyx{URa7Uf=V`6LsYC86#H|dyyQ?kZa0>CGrkN6%7o&%S%J|Y^f$p zQCV4U1gdCe)lZVu1OEQ z5?s^5>^Q2&P1?1t2~&J$3t#i&8;0dtrJLE~>(t~L+}3&>;CrP~O^#5WN0<|QLXp|v z67auh3v*^4FG;wOEl>l%S04#gSlT+$&k%SozLd7E_G0XoNLv!f(=fx}lOajPpJ=yf zXLhWd*m2lEpgu&&qBYYtEN>rQUD!Dr0XZL}1Z!qi_BcPexw*bL0qQ~m(Y0{-DYu1_iS)* z>Rn@hum*teSS8al%e(sC2*w}A3EzAoLQIc!Q5P2bT~07qC)`AhGmUUMD#ee4{&`ph@h zGQRE6H7J~tUFtw?65mT}P3)mm)r(p-eRf>!G^j++ZQJ{mVqJQz5lr+5eq%f7yLRKD zUog05<-9LwR4w`VkR~)fA9jJ6O~LcGgs9-2v5`hUkybHxU~n;Cta~M+dz|0;NocQe zFwZXGWT4d;4}*d7h+fpNadN#?Y4q;S9ph0@A~p%kR3j`>PM>NA(>2{;6g~E~1&EaT zHKRuywrLnSOYd(6gmdvKI&VBMLw8Mn^AELj-rQ106~cHoGm5#C`p^bXrRG_{J6vV_ zMGx+9leP*-XuVZ~k;`vNMjYT#W_f#x8#Zl+TA}q2V5IWMv zH0a?q;-N^t1>X?Cu^-l9iBRTy@VkbYAiQCb_5F_@c3y$bBYVEF^=o-3cN0T|K|~OD`vL~ z+_)>BRN-a13BLcfzBmInQ`;NR-5g$(ZMi5dqDMD~>rF4ztd_6gl?NH{hu00S@%g7W z%JS(NQ>DOD;1Lnv`OsKp-&O5mAQl$)@E&tPpr#dPMqNl==Vt|#zn`x$bGud4mPE0W zo0P2xeSSx-;ij&eSen1+8HnlMl`aZVcFMl$HI$?Rxp|?67U_@O6i zZW4FjfRK;?Psztx0I=D!xU1akh8cTFR#KRs8%HM+#58nFE>5ir54V1x!eI-lFDTB6 z!SX5qL|pqyiV?siUFRR*Vaod{V00_Gb2Uz+E~UIM(S(0CpsXlr9>YxneD!;01H|h> z)8oQ|`~yQ1vNS+xJ5ppHRpjY6^eX{-u&Mf`iK=r$Mhk)8d)FMtiYY1^z ztCN$3u4+uj?P{G{Q&&@WvYQ?4YpHfQDgY`R8&&n4W78+>istBKZFy#TdV6+#_t?_c G&j0` Date: Wed, 13 Dec 2023 19:07:00 +0200 Subject: [PATCH 2/5] Updated on 2026-08-14 --- .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 8 ++++---- .../feature/swap/converters/TokensDataConverter.kt | 10 ++++------ .../tangem/feature/swap/viewmodels/SwapViewModel.kt | 6 ++++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3754e0c835..631d3e6d51 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -102,15 +102,15 @@ internal class SwapInteractorImpl @Inject constructor( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.from }, - tokenInfoForAvailable = { it.to }, + tokenInfoForFilter = { it.to }, + tokenInfoForAvailable = { it.from }, ), toGroup = getToCurrenciesGroup( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.to }, - tokenInfoForAvailable = { it.from }, + tokenInfoForFilter = { it.from }, + tokenInfoForAvailable = { it.to }, ), allProviders = pairsLeast.allProviders, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 6f42bbd1d2..b6c99aee61 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,10 +2,7 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState -import com.tangem.core.ui.extensions.getTintForTokenIcon -import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -15,6 +12,7 @@ import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState +import com.tangem.feature.swap.presentation.R import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList @@ -26,8 +24,8 @@ class TokensDataConverter( ) : Converter { override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { - val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource - val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource + val availableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) + val unavailableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) return SwapSelectTokenStateHolder( availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index a36badc51e..06270b52d9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -653,12 +653,18 @@ internal class SwapViewModel @Inject constructor( private fun onChangeCardsClicked() { val newFromToken = dataState.toCryptoCurrency val newToToken = dataState.fromCryptoCurrency + + if (newFromToken != null && newToToken != null) { dataState = dataState.copy( fromCryptoCurrency = newFromToken, toCryptoCurrency = newToToken, ) isOrderReversed = !isOrderReversed + dataState.tokensDataState?.let { + updateTokensState(it) + } + val decimals = newFromToken.currency.decimals lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) uiState = stateBuilder.updateSwapAmount( From 7da444caf1843c899bd47f786aae3341e2f33e3d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 21:47:11 +0200 Subject: [PATCH 3/5] Updated on 2026-08-14 --- .../feature/swap/models/SwapStateHolder.kt | 6 +- .../tangem/feature/swap/ui/StateBuilder.kt | 31 +++-- .../feature/swap/ui/SwapScreenContent.kt | 45 ++++--- .../feature/swap/viewmodels/SwapViewModel.kt | 113 +++++++++--------- 4 files changed, 115 insertions(+), 80 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index ae4d9ba3cf..1502568f92 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -19,7 +19,7 @@ data class SwapStateHolder( val blockchainId: String, // not the same as networkId, its local id in app val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, - val updateInProgress: Boolean = false, + val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, val fee: FeeItemState = FeeItemState.Empty, @@ -131,4 +131,8 @@ sealed interface SwapWarning { enum class GenericWarningType { NETWORK, OTHER +} + +enum class ChangeCardsButtonState { + ENABLED, DISABLED, UPDATE_IN_PROGRESS } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 18933ba236..5564b792a7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -84,7 +84,7 @@ internal class StateBuilder( onBackClicked = actions.onBackClicked, onChangeCardsClicked = actions.onChangeCardsClicked, onMaxAmountSelected = actions.onMaxAmountSelected, - updateInProgress = true, + changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), ) @@ -138,7 +138,7 @@ internal class StateBuilder( loading = false, onClick = { }, ), - updateInProgress = false, + changeCardsButtonState = ChangeCardsButtonState.DISABLED, ) } @@ -186,7 +186,7 @@ internal class StateBuilder( swapButton = SwapButton(enabled = false, loading = true, onClick = {}), providerState = ProviderState.Loading(), permissionState = uiStateHolder.permissionState, - updateInProgress = true, + changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, ) } @@ -207,6 +207,7 @@ internal class StateBuilder( bestRatedProviderId: String, isManyProviders: Boolean, selectedFeeType: FeeType, + isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -257,7 +258,11 @@ internal class StateBuilder( loading = false, onClick = actions.onSwapClick, ), - updateInProgress = false, + changeCardsButtonState = if (isReverseSwapPossible) { + ChangeCardsButtonState.ENABLED + } else { + ChangeCardsButtonState.DISABLED + }, providerState = swapProvider.convertToContentClickableProviderState( isBestRate = bestRatedProviderId == swapProvider.providerId, fromTokenInfo = quoteModel.fromTokenInfo, @@ -360,6 +365,7 @@ internal class StateBuilder( fromToken: TokenSwapInfo, toToken: CryptoCurrencyStatus?, dataError: DataError, + isReverseSwapPossible: Boolean ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -409,7 +415,11 @@ internal class StateBuilder( loading = false, onClick = actions.onSwapClick, ), - updateInProgress = false, + changeCardsButtonState = if (isReverseSwapPossible) { + ChangeCardsButtonState.ENABLED + } else { + ChangeCardsButtonState.DISABLED + }, providerState = providerState, ) } @@ -475,6 +485,7 @@ internal class StateBuilder( fun createQuotesEmptyAmountState( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, + isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -514,7 +525,11 @@ internal class StateBuilder( loading = false, onClick = { }, ), - updateInProgress = false, + changeCardsButtonState = if (isReverseSwapPossible) { + ChangeCardsButtonState.ENABLED + } else { + ChangeCardsButtonState.DISABLED + }, providerState = ProviderState.Empty(), ) } @@ -538,7 +553,7 @@ internal class StateBuilder( fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( - updateInProgress = true, + changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, ) } @@ -713,7 +728,7 @@ internal class StateBuilder( onClick = onAlertClick, type = if (txState is TxState.NetworkError) GenericWarningType.NETWORK else GenericWarningType.OTHER, ), - updateInProgress = false, + changeCardsButtonState = ChangeCardsButtonState.ENABLED, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 8d1fe2e6df..cab2bb74d7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -195,23 +195,36 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { contentColor = TangemTheme.colors.text.primary1, modifier = modifier.size(TangemTheme.dimens.size48), onClick = state.onChangeCardsClicked, - enabled = !state.updateInProgress, + enabled = state.changeCardsButtonState == ChangeCardsButtonState.ENABLED, ) { - if (state.updateInProgress) { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .padding(TangemTheme.dimens.spacing14), - color = TangemTheme.colors.icon.primary1, - strokeWidth = TangemTheme.dimens.size2, - ) - } else { - Icon( - painter = painterResource(id = R.drawable.ic_exchange_vertical_24), - contentDescription = null, - tint = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - ) + when (state.changeCardsButtonState) { + ChangeCardsButtonState.UPDATE_IN_PROGRESS -> { + CircularProgressIndicator( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .padding(TangemTheme.dimens.spacing14), + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } + ChangeCardsButtonState.ENABLED -> { + Icon( + painter = painterResource(id = R.drawable.ic_exchange_vertical_24), + contentDescription = null, + tint = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) + } + ChangeCardsButtonState.DISABLED -> { + Icon( + painter = painterResource(id = R.drawable.ic_exchange_vertical_24), + contentDescription = null, + tint = TangemTheme.colors.text.disabled, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) + } + + } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 06270b52d9..0bd154c2c6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -60,8 +60,8 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { - private val initialCryptoCurrency: CryptoCurrency = savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] - ?: error("no expected parameter CryptoCurrency found`") + private val initialCryptoCurrency: CryptoCurrency = + savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter CryptoCurrency found`") private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus private var isBalanceHidden = true @@ -114,15 +114,12 @@ internal class SwapViewModel @Inject constructor( } override fun onCreate(owner: LifecycleOwner) { - getBalanceHidingSettingsUseCase() - .flowWithLifecycle(owner.lifecycle) - .onEach { + getBalanceHidingSettingsUseCase().flowWithLifecycle(owner.lifecycle).onEach { isBalanceHidden = it.isBalanceHidden withContext(dispatchers.main) { uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } - } - .launchIn(viewModelScope) + }.launchIn(viewModelScope) } override fun onCleared() { @@ -175,19 +172,18 @@ internal class SwapViewModel @Inject constructor( selectedCurrency = null, ) - uiState = - stateBuilder.createInitialErrorState( - uiState, - (it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code, - ) { - 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() + } } } } @@ -216,7 +212,7 @@ internal class SwapViewModel @Inject constructor( } private fun updateTokensState(dataState: TokensDataStateExpress) { - val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup + val tokensDataState = if (isOrderReversed) dataState.fromGroup else dataState.toGroup uiState = stateBuilder.addTokensToState( uiState = uiState, tokensDataState = tokensDataState, @@ -295,14 +291,12 @@ internal class SwapViewModel @Inject constructor( if (providersState.isNotEmpty()) { val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) - val successStates = providersState - .getLastLoadedSuccessStates() + val successStates = providersState.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(successStates) uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, pricesLowerBest = pricesLowerBest, - tokenSwapInfoForProviders = successStates.entries - .associate { it.key.providerId to it.value.toTokenInfo }, + tokenSwapInfoForProviders = successStates.entries.associate { it.key.providerId to it.value.toTokenInfo }, ) } else { Timber.e("Accidentally empty quotes list") @@ -329,6 +323,7 @@ internal class SwapViewModel @Inject constructor( bestRatedProviderId = bestRatedProviderId, isManyProviders = dataState.lastLoadedSwapStates.size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + isReverseSwapPossible = isReverseSwapPossible() ) if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { analyticsEventHandler.send( @@ -343,6 +338,7 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.createQuotesEmptyAmountState( uiStateHolder = uiState, emptyAmountState = state, + isReverseSwapPossible = isReverseSwapPossible(), ) } is SwapState.SwapError -> { @@ -353,6 +349,7 @@ internal class SwapViewModel @Inject constructor( fromToken = state.fromTokenInfo, toToken = dataState.toCryptoCurrency, dataError = state.error, + isReverseSwapPossible = isReverseSwapPossible(), ) sendErrorAnalyticsEvent(state.error, provider) } @@ -419,16 +416,15 @@ internal class SwapViewModel @Inject constructor( } private fun selectDefaultFee(state: SwapState.QuotesLoadedState): TxFee? { - return dataState.selectedFee - ?: when (val txFee = state.txFee) { - TxFeeState.Empty -> null - is TxFeeState.MultipleFeeState -> { - txFee.normalFee - } - is TxFeeState.SingleFeeState -> { - txFee.fee - } + return dataState.selectedFee ?: when (val txFee = state.txFee) { + TxFeeState.Empty -> null + is TxFeeState.MultipleFeeState -> { + txFee.normalFee } + is TxFeeState.SingleFeeState -> { + txFee.fee + } + } } @Suppress("LongMethod") @@ -453,8 +449,7 @@ internal class SwapViewModel @Inject constructor( includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, fee = requireNotNull(dataState.selectedFee), ) - } - .onSuccess { + }.onSuccess { when (it) { is TxState.TxSent -> { val url = blockchainInteractor.getExplorerTransactionLink( @@ -499,8 +494,7 @@ internal class SwapViewModel @Inject constructor( } } } - } - .onFailure { + }.onFailure { Timber.e(it) startLoadingQuotesFromLastState() makeDefaultAlert() @@ -539,8 +533,7 @@ internal class SwapViewModel @Inject constructor( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, - forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token) - ?.contractAddress + forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)?.contractAddress ?: "", fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { "dataState.fromCurrency might not be null" @@ -554,8 +547,7 @@ internal class SwapViewModel @Inject constructor( }, ), ) - } - .onSuccess { + }.onSuccess { when (it) { is TxState.TxSent -> { uiState = stateBuilder.loadingPermissionState(uiState) @@ -569,8 +561,7 @@ internal class SwapViewModel @Inject constructor( } } } - } - .onFailure { + }.onFailure { makeDefaultAlert() } } @@ -789,8 +780,8 @@ internal class SwapViewModel @Inject constructor( }, onClickFee = { val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL - val txFeeState = dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState - ?: return@UiActions + val txFeeState = + dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState ?: return@UiActions uiState = stateBuilder.showSelectFeeBottomSheet( uiState = uiState, selectedFee = selectedFee, @@ -868,9 +859,7 @@ internal class SwapViewModel @Inject constructor( private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? { // finding best quotes return state.minByOrNull { - if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && - !it.value.toTokenInfo.amountFiat.isNullOrZero() - ) { + if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && !it.value.toTokenInfo.amountFiat.isNullOrZero()) { it.value.fromTokenInfo.amountFiat.divide( it.value.toTokenInfo.amountFiat, it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, @@ -900,11 +889,9 @@ internal class SwapViewModel @Inject constructor( } private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> + return getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( + }.stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default, @@ -937,11 +924,27 @@ internal class SwapViewModel @Inject constructor( } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { - return this - .filter { it.value is SwapState.QuotesLoadedState } + return this.filter { it.value is SwapState.QuotesLoadedState } .mapValues { it.value as SwapState.QuotesLoadedState } } + private fun isReverseSwapPossible(): Boolean { + val from = dataState.fromCryptoCurrency ?: return false + val to = dataState.toCryptoCurrency ?: return false + + val currenciesGroup = if (isOrderReversed) { + dataState.tokensDataState?.toGroup + } else { + dataState.tokensDataState?.fromGroup + } ?: return false + + val chosen = if (isOrderReversed) from else to + + return currenciesGroup.available + .map { it.currencyStatus.currency } + .contains(chosen.currency) + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 16a4167be4a5ebfeea060397b09ba3efa35a95ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 21:53:03 +0200 Subject: [PATCH 4/5] Updated on 2026-08-14 --- .../swap/converters/TokensDataConverter.kt | 8 +- .../tangem/feature/swap/ui/StateBuilder.kt | 3 +- .../feature/swap/ui/SwapScreenContent.kt | 2 - .../feature/swap/viewmodels/SwapViewModel.kt | 144 +++++++++--------- 4 files changed, 80 insertions(+), 77 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index b6c99aee61..895fcb1ea5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -24,8 +24,12 @@ class TokensDataConverter( ) : Converter { override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { - val availableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) - val unavailableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) + val availableTitle = TokenToSelectState.Title( + resourceReference(R.string.exchange_tokens_available_tokens_header), + ) + val unavailableTitle = TokenToSelectState.Title( + resourceReference(R.string.exchange_tokens_available_tokens_header), + ) return SwapSelectTokenStateHolder( availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 5564b792a7..e66df2962d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -359,13 +359,14 @@ internal class StateBuilder( } } + @Suppress("LongParameterList") fun createQuotesErrorState( uiStateHolder: SwapStateHolder, swapProvider: SwapProvider, fromToken: TokenSwapInfo, toToken: CryptoCurrencyStatus?, dataError: DataError, - isReverseSwapPossible: Boolean + isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index cab2bb74d7..360cd4e016 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -223,8 +223,6 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { modifier = Modifier.padding(TangemTheme.dimens.spacing12), ) } - - } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 0bd154c2c6..b7606f80d0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -115,11 +115,11 @@ internal class SwapViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { getBalanceHidingSettingsUseCase().flowWithLifecycle(owner.lifecycle).onEach { - isBalanceHidden = it.isBalanceHidden - withContext(dispatchers.main) { - uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) - } - }.launchIn(viewModelScope) + isBalanceHidden = it.isBalanceHidden + withContext(dispatchers.main) { + uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) + } + }.launchIn(viewModelScope) } override fun onCleared() { @@ -296,7 +296,8 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, pricesLowerBest = pricesLowerBest, - tokenSwapInfoForProviders = successStates.entries.associate { it.key.providerId to it.value.toTokenInfo }, + tokenSwapInfoForProviders = successStates.entries + .associate { it.key.providerId to it.value.toTokenInfo }, ) } else { Timber.e("Accidentally empty quotes list") @@ -323,7 +324,7 @@ internal class SwapViewModel @Inject constructor( bestRatedProviderId = bestRatedProviderId, isManyProviders = dataState.lastLoadedSwapStates.size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - isReverseSwapPossible = isReverseSwapPossible() + isReverseSwapPossible = isReverseSwapPossible(), ) if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { analyticsEventHandler.send( @@ -450,55 +451,55 @@ internal class SwapViewModel @Inject constructor( fee = requireNotNull(dataState.selectedFee), ) }.onSuccess { - when (it) { - is TxState.TxSent -> { - val url = blockchainInteractor.getExplorerTransactionLink( - networkId = dataState.networkId, - txAddress = it.txAddress, - ) - uiState = stateBuilder.createSuccessState( - uiState = uiState, - txState = it, - dataState = dataState, - txUrl = url, - onExploreClick = { - val txHash = it.txAddress - if (txHash.isNotEmpty()) { - swapRouter.openUrl(url) - } + when (it) { + is TxState.TxSent -> { + val url = blockchainInteractor.getExplorerTransactionLink( + networkId = dataState.networkId, + txAddress = it.txAddress, + ) + uiState = stateBuilder.createSuccessState( + uiState = uiState, + txState = it, + dataState = dataState, + txUrl = url, + onExploreClick = { + val txHash = it.txAddress + if (txHash.isNotEmpty()) { + swapRouter.openUrl(url) + } + analyticsEventHandler.send( + event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + ) + }, + onStatusClick = { + val txExternalUrl = it.txExternalUrl + if (!txExternalUrl.isNullOrBlank()) { + swapRouter.openUrl(txExternalUrl) analyticsEventHandler.send( - event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), ) - }, - onStatusClick = { - val txExternalUrl = it.txExternalUrl - if (!txExternalUrl.isNullOrBlank()) { - swapRouter.openUrl(txExternalUrl) - analyticsEventHandler.send( - event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), - ) - } - }, - ) - sendSuccessEvent() + } + }, + ) + sendSuccessEvent() - swapRouter.openScreen(SwapNavScreen.Success) - } - is TxState.UserCancelled -> { - startLoadingQuotesFromLastState() - } - else -> { - startLoadingQuotesFromLastState() - uiState = stateBuilder.createErrorTransaction(uiState, it) { - uiState = stateBuilder.clearAlert(uiState) - } + swapRouter.openScreen(SwapNavScreen.Success) + } + is TxState.UserCancelled -> { + startLoadingQuotesFromLastState() + } + else -> { + startLoadingQuotesFromLastState() + uiState = stateBuilder.createErrorTransaction(uiState, it) { + uiState = stateBuilder.clearAlert(uiState) } } - }.onFailure { - Timber.e(it) - startLoadingQuotesFromLastState() - makeDefaultAlert() } + }.onFailure { + Timber.e(it) + startLoadingQuotesFromLastState() + makeDefaultAlert() + } } } @@ -533,8 +534,8 @@ internal class SwapViewModel @Inject constructor( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, - forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)?.contractAddress - ?: "", + forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token) + ?.contractAddress ?: "", fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { "dataState.fromCurrency might not be null" }, @@ -548,22 +549,22 @@ internal class SwapViewModel @Inject constructor( ), ) }.onSuccess { - when (it) { - is TxState.TxSent -> { - uiState = stateBuilder.loadingPermissionState(uiState) - uiState = stateBuilder.dismissBottomSheet(uiState) - startLoadingQuotesFromLastState(isSilent = true) - } - is TxState.UserCancelled -> Unit - else -> { - uiState = stateBuilder.createErrorTransaction(uiState, it) { - uiState = stateBuilder.clearAlert(uiState) - } + when (it) { + is TxState.TxSent -> { + uiState = stateBuilder.loadingPermissionState(uiState) + uiState = stateBuilder.dismissBottomSheet(uiState) + startLoadingQuotesFromLastState(isSilent = true) + } + is TxState.UserCancelled -> Unit + else -> { + uiState = stateBuilder.createErrorTransaction(uiState, it) { + uiState = stateBuilder.clearAlert(uiState) } } - }.onFailure { - makeDefaultAlert() } + }.onFailure { + makeDefaultAlert() + } } } @@ -645,7 +646,6 @@ internal class SwapViewModel @Inject constructor( val newFromToken = dataState.toCryptoCurrency val newToToken = dataState.fromCryptoCurrency - if (newFromToken != null && newToToken != null) { dataState = dataState.copy( fromCryptoCurrency = newFromToken, @@ -890,12 +890,12 @@ internal class SwapViewModel @Inject constructor( private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) } private fun findSwapProviders(fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus): List { From 38c9940d7d05d64ea66e7080e526fc432c90b210 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Dec 2023 13:17:02 +0300 Subject: [PATCH 5/5] Updated on 2026-08-14 --- .../tokens/GetCurrencyWarningsUseCase.kt | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index f658706441..3090438e08 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -16,6 +16,8 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import java.math.BigDecimal @@ -89,30 +91,36 @@ class GetCurrencyWarningsUseCase( cryptoStatuses.fold( ifLeft = { null }, ifRight = { cryptoCurrencyStatuses -> - val pairs = swapRepository.getPairs( - LeastTokenInfo( - contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, - ), - cryptoCurrencyStatuses.map { it.currency }, - ) + val pairs = runCatching(dispatchers.io) { + swapRepository.getPairs( + LeastTokenInfo( + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = currency.network.backendId, + ), + cryptoCurrencyStatuses.map { it.currency }, + ) + }.getOrNull()?.pairs ?: emptyList() + val filteredCurrencies = cryptoCurrencyStatuses.filterNot { - it.currency.network.backendId == currency.network.backendId + it.currency.id == currency.id } - val currencyPairs = pairs.pairs.filter { + val currencyPairs = pairs.filter { it.from.network == currency.network.backendId || it.to.network == currency.network.backendId } val isExchangeable = currencyPairs.any { pair -> - val availablePair = if (currencyStatus.value.amount.isZero()) { - filteredCurrencies.filterNot { it.value.amount.isZero() } + val availablePair = if (currencyStatus.value.amount.isNullOrZero()) { + filteredCurrencies.filterNot { it.value.amount.isNullOrZero() } } else { filteredCurrencies } availablePair .any { - it.currency.network.backendId == pair.to.network || - it.currency.network.backendId == pair.from.network + ( + it.currency.network.backendId == pair.to.network || + it.currency.network.backendId == pair.from.network + ) && + !it.value.amount.isNullOrZero() } } if (isExchangeable) {