Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-14 14:06:56 +03:00
commit ffda552327
12 changed files with 223 additions and 191 deletions

View file

@ -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),

View file

@ -1,20 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="34dp"
android:viewportWidth="34"
android:viewportHeight="34">
<path
android:pathData="M22,0C28.628,0 34,5.373 34,12C34,18.628 28.628,24 22,24C15.373,24 10,18.628 10,12C10,5.373 15.373,0 22,0Z"
android:fillColor="#F5AC37"/>
<path
android:pathData="M22.442,12.847H27.002C27.099,12.847 27.145,12.847 27.152,12.72C27.189,12.256 27.189,11.79 27.152,11.325C27.152,11.235 27.107,11.198 27.01,11.198H17.935C17.823,11.198 17.792,11.235 17.792,11.34V12.675C17.792,12.847 17.792,12.847 17.972,12.847H22.442ZM26.643,9.638C26.656,9.604 26.656,9.566 26.643,9.533C26.567,9.367 26.477,9.209 26.372,9.06C26.214,8.807 26.028,8.573 25.817,8.363C25.717,8.236 25.601,8.122 25.472,8.025C24.823,7.473 24.051,7.082 23.221,6.885C22.803,6.791 22.375,6.746 21.947,6.75H17.919C17.807,6.75 17.792,6.795 17.792,6.893V9.555C17.792,9.667 17.792,9.698 17.934,9.698H26.589C26.589,9.698 26.664,9.682 26.679,9.638H26.642H26.643ZM26.643,14.407C26.515,14.393 26.387,14.393 26.26,14.407H17.942C17.83,14.407 17.792,14.407 17.792,14.558V17.16C17.792,17.28 17.792,17.31 17.942,17.31H21.782C21.966,17.325 22.149,17.312 22.329,17.273C22.887,17.233 23.435,17.112 23.958,16.913C24.147,16.847 24.331,16.761 24.504,16.658H24.557C25.457,16.19 26.188,15.455 26.649,14.552C26.649,14.552 26.702,14.438 26.643,14.408V14.407ZM16.285,18.66V18.615V16.868V16.275V14.513C16.285,14.415 16.285,14.4 16.165,14.4H14.538C14.447,14.4 14.41,14.4 14.41,14.281V12.856H16.15C16.247,12.856 16.285,12.856 16.285,12.728V11.318C16.285,11.228 16.285,11.206 16.165,11.206H14.538C14.447,11.206 14.41,11.206 14.41,11.086V9.766C14.41,9.683 14.41,9.661 14.53,9.661H16.142C16.255,9.661 16.285,9.661 16.285,9.519V5.476C16.285,5.356 16.285,5.326 16.435,5.326H22.06C22.468,5.342 22.874,5.387 23.275,5.461C24.102,5.614 24.896,5.909 25.622,6.331C26.104,6.615 26.547,6.957 26.942,7.351C27.239,7.66 27.507,7.993 27.744,8.349C27.98,8.709 28.177,9.094 28.33,9.496C28.349,9.601 28.45,9.672 28.555,9.654H29.897C30.069,9.654 30.069,9.654 30.077,9.819V11.049C30.077,11.169 30.032,11.199 29.912,11.199H28.876C28.772,11.199 28.741,11.199 28.749,11.334C28.79,11.791 28.79,12.25 28.749,12.706C28.749,12.834 28.749,12.849 28.892,12.849H30.076C30.129,12.917 30.076,12.984 30.076,13.052C30.084,13.139 30.084,13.227 30.076,13.314V14.222C30.076,14.349 30.039,14.387 29.926,14.387H28.509C28.41,14.368 28.314,14.431 28.291,14.53C27.954,15.407 27.413,16.194 26.716,16.824C26.461,17.054 26.193,17.27 25.913,17.469C25.613,17.642 25.321,17.822 25.013,17.965C24.447,18.219 23.853,18.407 23.243,18.527C22.664,18.63 22.077,18.677 21.488,18.669H16.283V18.662L16.285,18.66Z"
android:fillColor="#FEFEFD"/>
<path
android:pathData="M8.063,10.661C3.37,12.29 0,16.751 0,22C0,28.627 5.373,34 12,34C17.249,34 21.71,30.631 23.339,25.937C22.898,25.979 22.452,26 22,26C14.269,26 8,19.732 8,12C8,11.548 8.021,11.101 8.063,10.661Z"
android:fillColor="#8247E5"
android:fillType="evenOdd"/>
<path
android:pathData="M8.324,15.007C8.19,15.022 8.055,15.061 7.927,15.125L4.501,17.131C4.167,17.298 4,17.632 4,17.966V21.893C4,22.227 4.167,22.562 4.501,22.77L7.885,24.734C8.178,24.901 8.554,24.901 8.888,24.734L11.186,23.397L12.738,22.498C12.328,22.136 11.939,21.751 11.574,21.344L11.186,21.559L8.888,22.937C8.595,23.105 8.219,23.105 7.885,22.937L6.089,21.851C5.796,21.684 5.587,21.35 5.587,20.974V18.885C5.587,18.551 5.754,18.217 6.089,18.008L7.885,16.964C8.178,16.796 8.554,16.796 8.888,16.964L8.911,16.978C8.668,16.341 8.472,15.683 8.324,15.007ZM20,25.858C19.423,25.776 18.857,25.658 18.305,25.507C18.228,25.711 18.083,25.895 17.87,26.029L16.115,27.073C15.822,27.24 15.446,27.24 15.112,27.073L13.316,26.029C13.023,25.862 12.814,25.528 12.814,25.152V23.815L11.227,24.734V26.112C11.227,26.447 11.394,26.781 11.729,26.99L15.112,28.953C15.211,29.01 15.319,29.047 15.432,29.065C15.654,29.101 15.894,29.064 16.115,28.953L19.499,26.99C19.791,26.823 20,26.488 20,26.112V25.858Z"
android:fillColor="#FEFEFD"
android:fillType="evenOdd"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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) {

View file

@ -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,
)

View file

@ -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,12 @@ class TokensDataConverter(
) : Converter<CurrenciesGroup, SwapSelectTokenStateHolder> {
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()

View file

@ -19,7 +19,7 @@ data class SwapStateHolder(
val blockchainId: String, // not the same as networkId, its local id in app
val warnings: List<SwapWarning> = 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
}

View file

@ -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,
@ -354,12 +359,14 @@ internal class StateBuilder(
}
}
@Suppress("LongParameterList")
fun createQuotesErrorState(
uiStateHolder: SwapStateHolder,
swapProvider: SwapProvider,
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 +416,11 @@ internal class StateBuilder(
loading = false,
onClick = actions.onSwapClick,
),
updateInProgress = false,
changeCardsButtonState = if (isReverseSwapPossible) {
ChangeCardsButtonState.ENABLED
} else {
ChangeCardsButtonState.DISABLED
},
providerState = providerState,
)
}
@ -475,6 +486,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 +526,11 @@ internal class StateBuilder(
loading = false,
onClick = { },
),
updateInProgress = false,
changeCardsButtonState = if (isReverseSwapPossible) {
ChangeCardsButtonState.ENABLED
} else {
ChangeCardsButtonState.DISABLED
},
providerState = ProviderState.Empty(),
)
}
@ -538,7 +554,7 @@ internal class StateBuilder(
fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
updateInProgress = true,
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
)
}
@ -713,7 +729,7 @@ internal class StateBuilder(
onClick = onAlertClick,
type = if (txState is TxState.NetworkError) GenericWarningType.NETWORK else GenericWarningType.OTHER,
),
updateInProgress = false,
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
)
}

View file

@ -195,23 +195,34 @@ 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),
)
}
}
}
}

View file

@ -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 {
isBalanceHidden = it.isBalanceHidden
withContext(dispatchers.main) {
uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden)
}
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,8 +291,7 @@ 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,
@ -329,6 +324,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 +339,7 @@ internal class SwapViewModel @Inject constructor(
uiState = stateBuilder.createQuotesEmptyAmountState(
uiStateHolder = uiState,
emptyAmountState = state,
isReverseSwapPossible = isReverseSwapPossible(),
)
}
is SwapState.SwapError -> {
@ -353,6 +350,7 @@ internal class SwapViewModel @Inject constructor(
fromToken = state.fromTokenInfo,
toToken = dataState.toCryptoCurrency,
dataError = state.error,
isReverseSwapPossible = isReverseSwapPossible(),
)
sendErrorAnalyticsEvent(state.error, provider)
}
@ -419,16 +417,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,58 +450,56 @@ internal class SwapViewModel @Inject constructor(
includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount,
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)
}
}.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)
}
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()
}
}
}
@ -540,8 +535,7 @@ internal class SwapViewModel @Inject constructor(
"dataState.approveDataModel might not be null"
},
forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)
?.contractAddress
?: "",
?.contractAddress ?: "",
fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) {
"dataState.fromCurrency might not be null"
},
@ -554,25 +548,23 @@ 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)
}
}.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)
}
}
}
.onFailure {
makeDefaultAlert()
}
}.onFailure {
makeDefaultAlert()
}
}
}
@ -653,12 +645,17 @@ 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(
@ -783,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,
@ -862,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,
@ -894,15 +889,13 @@ internal class SwapViewModel @Inject constructor(
}
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
return getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
}
private fun findSwapProviders(fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus): List<SwapProvider> {
@ -931,11 +924,27 @@ internal class SwapViewModel @Inject constructor(
}
private fun Map<SwapProvider, SwapState>.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 = ""

View file

@ -46,7 +46,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
config = NotificationConfig(
title = resourceReference(id = R.string.token_swap_promotion_title),
subtitle = resourceReference(id = R.string.token_swap_promotion_message),
iconResId = R.drawable.ic_swap_promo_34,
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_banner_background,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(

View file

@ -173,7 +173,7 @@ sealed class WalletNotification(val config: NotificationConfig) {
config = 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,
onCloseClick = onCloseClick,
),